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-Originresponse header decides readability; - Preflight: custom headers or non-simple methods trigger an
OPTIONSprobe first; the real request only follows if it passes.
Common misconfigurations
- Blindly returning *: combined with credentials it conflicts and effectively opens to any origin;
- Missing preflight headers: only configuring the main request, so OPTIONS is rejected and the real request never arrives;
- 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
- Duplicate headers: Nginx and the backend framework each add
Access-Control-Allow-Origin. The browser accepts only one value and reportsThe 'Access-Control-Allow-Origin' header contains multiple values. Inspect withcurl -Iand keep a single source. - OPTIONS returns 405: the preflight is rejected as a normal request, so the real request never leaves. Fix: have the gateway/framework answer
OPTIONSwith 204 and echo the allowed methods and headers. - Fails only with cookies: cross-origin needs both
credentials: 'include'on the client andAccess-Control-Allow-Credentials: trueon the server, andAllow-Origincannot 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, orOriginvalidation.
Related headers
Access-Control-Allow-Origincontrols reading only, not whether the request is sent;Access-Control-Max-Agecaches the preflight result; too small and every request pays for an OPTIONS round trip;Vary: Originis 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
- 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.
- Read the full response headers: the reason is usually stated there, and reading them beats inferring from the error text.
- 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.
- 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.
- 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.