CORS, demystified
Cross-Origin Resource Sharing, usually called CORS, is a browser enforcement mechanism controlled by HTTP headers. It does not protect a server from receiving requests. It tells t…
Cross-Origin Resource Sharing, usually called CORS, is a browser enforcement mechanism controlled by HTTP headers. It does not protect a server from receiving requests. It tells the browser whether frontend code from one origin may read a response from another origin.
Origin is the key boundary
An origin is the combination of scheme, host and port. https://example.com and https://api.example.com are different origins. http://example.com and https://example.com are also different origins because the scheme is different.
Browsers apply the same-origin policy to protect one site from reading another site's data. CORS is a controlled relaxation of that policy for selected cross-origin requests.
CORS is enforced by browsers
A server can receive a cross-origin request even when CORS is misconfigured. The browser may still block frontend JavaScript from reading the response. That distinction matters when debugging: a CORS error in the console is usually about browser access to the response, not whether the request reached the server.
Non-browser clients such as curl, backend services and many mobile HTTP clients do not enforce browser CORS rules in the same way. Do not use CORS as an API authentication control.
Simple requests and preflight requests
Some cross-origin requests are sent directly by the browser. Others require a preflight. A preflight is an OPTIONS request sent before the actual request so the browser can ask the server whether the cross-origin operation is allowed.
A request avoids preflight only when it uses GET, HEAD or POST, sets only CORS-safelisted request headers, and uses a Content-Type of application/x-www-form-urlencoded, multipart/form-data or text/plain. Anything else triggers preflight. This is why a cross-origin request with an Authorization header or Content-Type: application/json is preflighted, which surprises many developers.
The preflight response must authorise the method, headers and origin. If it does not, the browser blocks the actual request or blocks access to the response.
The core response headers
Access-Control-Allow-Origin tells the browser which origin may read the response. It can be a specific origin or the wildcard *, but * cannot be used for credentialed browser requests.
Access-Control-Allow-Methods lists methods allowed for the actual request after preflight. Access-Control-Allow-Headers lists request headers allowed for the actual request after preflight. Access-Control-Max-Age tells the browser how long it may cache the preflight result.
Access-Control-Allow-Credentials: true allows the browser to expose a credentialed response when the request includes credentials and the origin is explicitly allowed.
Credentials change the rules
Credentials include cookies, TLS client certificates and HTTP authentication entries. By default, browsers do not send credentials on cross-origin fetch or XMLHttpRequest calls unless the code opts in, for example with credentials set to include.
When credentials are used, the server must return a specific Access-Control-Allow-Origin value. It must not use *. It must also return Access-Control-Allow-Credentials: true for the browser to expose the response to JavaScript.
Because cookies can be sent automatically, credentialed CORS needs careful server side authorisation. CORS does not replace CSRF protection for state changing requests.
Dynamic origin reflection is risky
Some systems read the Origin request header and echo it back as Access-Control-Allow-Origin. That is only safe when the origin is checked against a strict allowlist first.
Never combine unrestricted origin reflection with credentials. That pattern can let an attacker's site read responses that were intended only for a trusted frontend.
Also add Vary: Origin when the response varies by Origin and can pass through shared caches. Without Vary, a cache can reuse a response authorised for one origin in a different origin context.
CORS failures are usually configuration failures
A failed CORS request is often caused by one missing header on the preflight response, a redirect during preflight, an origin mismatch, a method not listed in Access-Control-Allow-Methods, or an application error path that omits CORS headers.
Check the network panel, not only the console message. The console often reports the browser level failure, while the network panel shows whether the preflight was sent, what status came back and which headers were present.
CORS is not a security boundary for APIs
CORS can limit which browser origins can read responses. It does not prove who the user is. It does not stop direct requests from scripts, servers or command line tools. It does not validate permissions.
Every protected endpoint still needs authentication and authorisation. Treat CORS as browser integration policy, not access control.
Practical configuration
Allow only the frontend origins that need browser access. Return exact origins rather than broad wildcards for application APIs. Keep allowed methods and headers narrow. Enable credentials only when the browser must send cookies or HTTP credentials.
For public, unauthenticated, read-only resources that are safe for any site to read, Access-Control-Allow-Origin: * can be appropriate. For private APIs, use explicit origins and real authentication.
Conclusion
CORS is easiest to understand when separated from authentication. The server receives requests according to normal HTTP rules. The browser decides whether frontend JavaScript may read the response according to CORS headers. Configure it narrowly, test the preflight path and never treat it as the only protection on an API.
