What is CORS?
|
This page documents general Cross-Origin Resource Sharing (CORS) behavior as specified by the WHATWG Fetch Standard. Unlike the other reference sections on this site, no single reference book underpins it: the content was generated with the assistance of AI from general knowledge, and should be verified against the Fetch Standard’s CORS protocol section and MDN’s CORS guide before relying on it in production. |
Cross-Origin Resource Sharing (CORS) is a browser mechanism that relaxes the same-origin policy, at the server’s discretion, to let a web page make requests to a different origin than the one it was served from. This page covers what CORS actually protects, how the browser and server negotiate it, and how to validate origins correctly on the backend.
What CORS Is
CORS is enforced only by the browser. The browser’s default is to block a script running on one origin from
reading the response of a request it makes to a different origin — the same-origin policy. Two URLs share an
origin only when their protocol, hostname, and port all match; https://example.com and
http://example.com are different origins (different protocol), as are https://example.com and
https://api.example.com (different hostname), and https://example.com and https://example.com:8443
(different port). CORS is the server’s way of opting specific other origins back in.
Because CORS is enforced client-side, it is not a server-side security measure. It does not protect an API
from being called directly — a request made with curl, Postman, a mobile app’s HTTP client, or a script
running server-side never goes through a browser, so there is no same-origin policy to relax or CORS check to
perform in the first place. Anything an API exposes to a browser over CORS is just as reachable without it;
authentication and authorization are what actually gate access (see
CORS Does Not Replace Authentication).
The Origin Header
When a page’s script issues a cross-origin request, the browser automatically attaches an Origin header of the
form Origin: <scheme>://<host>[:<port>], e.g. Origin: https://app.example.com. This header:
-
Is set by the browser itself, not by page JavaScript — a script cannot spoof or omit it.
-
Carries only the requesting page’s origin (never a full path), which is exactly the information the server needs to decide whether to allow the request.
-
Is present on cross-origin requests, and on same-origin
POST/PUT/DELETE/PATCHrequests in most modern browsers, but not on same-originGET/HEADrequests.
The server inspects Origin and decides, per request, whether to answer with CORS headers that allow the
calling origin to read the response.
Simple vs. Preflight Requests
Not every cross-origin request is treated the same way. A request qualifies as a simple request — sent directly, with no preliminary check — only when all of the following hold:
-
The method is
GET,HEAD, orPOST. -
Only a small set of "CORS-safelisted" headers are set (e.g.
Accept,Accept-Language,Content-Language,Content-Typewith a restricted value). -
When present,
Content-Typeis one ofapplication/x-www-form-urlencoded,multipart/form-data, ortext/plain.
Anything else — PUT, DELETE, PATCH, Content-Type: application/json, or a custom header such as
Authorization or X-Requested-With — is not simple, and triggers a preflight: the browser first sends an
OPTIONS request to ask the server for permission, before sending the actual request at all.
For a simple request, the browser sends the request immediately and simply checks the response headers before letting the script read the body:
For a non-simple request, the browser performs the OPTIONS handshake first, and only sends the real request if
the server approves it:
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization Server-->>Browser: 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: content-type, authorization Note over Browser: Preflight approved Browser->>Server: PUT /api/data (Origin: https://app.example.com) Server-->>Browser: 200 OK + Access-Control-Allow-Origin: https://app.example.com Note over Browser: Origin allowed -- response body exposed to script
The preflight OPTIONS request carries Access-Control-Request-Method (the method the real request will use)
and, if any, Access-Control-Request-Headers (the custom headers it will send), so the server can approve or
reject the real request before it ever runs.
Server Response Headers
The server communicates what it allows through a small set of Access-Control-* response headers:
| Header | Meaning |
|---|---|
|
The origin (or |
|
Sent on the preflight response; the HTTP methods the server permits for this resource. |
|
Sent on the preflight response; the request headers the server permits beyond the CORS-safelisted set. |
|
Set to |
A subtlety worth internalizing: for a simple request, the actual request still runs on the server — CORS
headers do not stop it from executing — CORS only controls whether the browser lets the page’s script read the
response. A POST that mutates data still mutates it even if the browser then blocks the script from seeing the
result, which is exactly why CORS is not a substitute for real authorization. A preflighted (non-simple) request
is the exception: if the OPTIONS preflight is rejected, the browser never sends the real request at all.
Validating Origins on the Backend
The safe pattern is an explicit allowlist, checked against the incoming Origin header, with the response
varying per request:
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
]);
function corsMiddleware(req, res, next) {
const origin = req.headers.origin;
if (origin && allowedOrigins.has(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
}
// Tell caches/CDNs the response varies by request origin -- otherwise a
// cached response for one origin could be served to a different one.
res.setHeader("Vary", "Origin");
if (req.method === "OPTIONS") {
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
return res.status(204).end();
}
next();
}
This pattern is language/framework-agnostic — an equivalent allowlist check applies whether it is expressed as
Express middleware, a Django/DRF setting, a Spring CorsConfigurationSource bean, or any other backend’s CORS
configuration. The essentials are always the same: compare Origin against a known-good set (or a correctly
anchored pattern), echo back only that exact origin, and set Vary: Origin so shared caches don’t leak one
origin’s allowed response to another.
Common Mistakes
-
Access-Control-Allow-Origin:combined withAccess-Control-Allow-Credentials: true. Browsers reject this combination outright — a wildcard origin can never be paired with credentials. If it *did work, it would let any website read a logged-in user’s authenticated responses (cookies included) from the API, which is exactly the cross-site data theft the same-origin policy exists to prevent. -
Reflecting the received
Originheader back unvalidated. SettingAccess-Control-Allow-Origin: <whatever Origin the request sent>without checking it against an allowlist is functionally equivalent to allowing every origin, while looking like a real check. -
An unanchored allowlist regex. A pattern like
/example\.com/(no^/$anchors at all) matches far more than intended — it only checks thatexample.comappears somewhere in the string, so bothevil-example.comandexample.com.attacker.comslip through. Match the full origin string exactly (^https://example\.com$), or maintain an explicit set as in the example above. -
Relying on
Refererinstead ofOriginfor validation.Referercarries the full requesting URL (not just the origin), is not sent on every request (privacy settings,Referrer-Policy, and some proxies can strip or omit it), and was never designed as an access-control signal.Originis purpose-built for this check and is what browsers themselves rely on.
CORS Does Not Replace Authentication
CORS is a browser-side access-control layer that governs whether a script running on one origin may read a response from another — nothing more. It says nothing about who is making the request or whether they are allowed to perform it. Real access control still has to come from actual authentication and authorization: session cookies, bearer tokens, API keys, and the server-side checks built on top of them. Treat CORS configuration as a browser-compatibility concern for legitimate cross-origin clients, never as a substitute for verifying who is calling the API.