← Back to all articles

What CORS Actually Blocks: Same-Origin and Preflight

HTTPSecurityBeginner

What the same-origin policy is

Same scheme, host and port means same origin. For cross-origin frontend requests the browser decides whether to allow JS to read the response; it blocks reading, not sending.

Simple request vs preflight

  • Simple request: GET / some POSTs, no custom headers — sent directly, the Access-Control-Allow-Origin response header decides readability;
  • Preflight: custom headers or non-simple methods trigger an OPTIONS probe first; the real request only follows if it passes.

Common misconfigurations

  1. Blindly returning *: combined with credentials it conflicts and effectively opens to any origin;
  2. Missing preflight headers: only configuring the main request, so OPTIONS is rejected and the real request never arrives;
  3. Using a proxy to dodge CORS: works but moves the cross-origin issue server-side, where trust still must be controlled.

Real-world cases: three errors you will meet

  1. Duplicate headers: Nginx and the backend framework each add Access-Control-Allow-Origin. The browser accepts only one value and reports The 'Access-Control-Allow-Origin' header contains multiple values. Inspect with curl -I and keep a single source.
  2. OPTIONS returns 405: the preflight is rejected as a normal request, so the real request never leaves. Fix: have the gateway/framework answer OPTIONS with 204 and echo the allowed methods and headers.
  3. Fails only with cookies: cross-origin needs both credentials: 'include' on the client and Access-Control-Allow-Credentials: true on the server, and Allow-Origin cannot be * — it must echo the specific origin.

FAQ

Is CORS a backend vulnerability? No — it is a browser-enforced policy. The response headers only grant permission; without them the script cannot read the response. Why does it error even though the request was sent? Because reading is what gets blocked; the request already reached the server, so writes still need authentication and idempotency. Is a different port same-origin? No — scheme, host or port differing makes it cross-origin. How do I handle local development? Use a dev-server proxy (e.g. Vite's server.proxy) rather than loosening CORS. Is * enough? For public read-only APIs yes; anything with credentials or internal data needs an origin allowlist.

How CORS relates to CSRF

These two are routinely conflated although they point in opposite directions:

  • CORS protects reading: it decides whether site A may read site B's response — a browser mechanism protecting user data from cross-site reads;
  • CSRF attacks writing: the attacker does not need to read the response, only to make the browser send a request with the user's cookie, so permissive CORS is not the cause of CSRF;
  • Their defences are therefore different: CORS cannot stop CSRF — you need SameSite, a CSRF token, or Origin validation.

Related headers

  • Access-Control-Allow-Origin controls reading only, not whether the request is sent;
  • Access-Control-Max-Age caches the preflight result; too small and every request pays for an OPTIONS round trip;
  • Vary: Origin is essential when echoing the origin dynamically, or a CDN caches one origin's response and serves it to another.

Get these right and you open exactly the cross-origin access you intend without losing the permission boundary.

Debugging cross-origin problems step by step

  1. Check whether the request was sent: look in the network panel for the preflight. If it fails, the real request never leaves and editing business logic is pointless.
  2. Read the full response headers: the reason is usually stated there, and reading them beats inferring from the error text.
  3. Confirm the origin is echoed: with credentials the origin must match exactly and cannot be a wildcard; a wrong echo or a missing declaration fails.
  4. Reproduce from the command line: send the request with the origin header yourself and read the response, ruling out browser cache and frontend code to isolate server behaviour.
  5. Separate dev and production config: development often relaxes rules for convenience — make sure production has its own stricter configuration rather than shipping the debug one.

Cross-origin issues are configuration problems, not code bugs; this order narrows the search to one specific header.