Loading CORS Policy Explainer…
Paste headers and describe the request; the answer is worked out in your own browser and nothing is fetched. The one thing to hold on to before reading any of it: CORS decides who may read a response. It never decides who may send a request.
Paste the response headers above and the answer for this request appears here.
Nothing in these headers relaxes the same-origin policy, which is the correct default. Note what it does not do: a cross-site form post or image load still reaches this endpoint, cookies and all. CORS governs who may read a reply, not who may send a request.
Fetch Standard, CORS protocol
GET, HEAD, POST
accept, accept-language, content-language, content-type, range
application/x-www-form-urlencoded, multipart/form-data, text/plain
cache-control, content-language, content-length, content-type, expires, last-modified, pragma
Access-Control-Allow-OriginThe one origin allowed to read the response, or * for any of them. One value only — a list is not valid syntax.Fetch Standard, CORS protocolAccess-Control-Allow-CredentialsSet to true, permits the response to be read by a request that carried cookies or TLS client certificates.Fetch Standard, CORS protocolAccess-Control-Allow-MethodsAnswers a preflight with the methods permitted. Ignored on any response that is not a preflight.Fetch Standard, CORS-preflight fetchAccess-Control-Allow-HeadersAnswers a preflight with the request headers permitted. Also only read on a preflight response.Fetch Standard, CORS-preflight fetchAccess-Control-Expose-HeadersNames response headers the caller may read beyond the seven that are always readable.Fetch Standard, CORS-safelisted response-header nameAccess-Control-Max-AgeHow many seconds a preflight result may be cached. Browsers impose their own ceiling on it.Fetch Standard, CORS-preflight cacheVaryTells caches the response depends on a request header. Required in practice whenever the allowed origin is echoed back.RFC 9110 § 12.5.5Because the request is sent either way, a cross-site form post to an endpoint that changes something still changes it — the attacker simply never sees the reply. Tightening these headers does nothing about that. The defences are a SameSite cookie, a token the attacker cannot read, or checking the Origin header server-side.
Cross-origin resource sharing is a relaxation of the same-origin policy, not an addition to it. The browser sends the request either way; what these headers decide is whether the calling script is allowed to see the reply. When the check fails, the script receives a network error while your server has already received, parsed and acted on the request.
That one sentence resolves most of the confusion around the subject. It explains why a cross-site form post still reaches an endpoint no CORS header mentions, and why tightening Access-Control-Allow-Origin does nothing about cross-site request forgery. The defences there are a SameSite cookie, a token the attacker cannot read, or an Origin check performed by your own code.
Some requests go straight out. A GET, HEAD or POST that sets only safelisted headers, and whose Content-Type is form-encoded, multipart or plain text, is indistinguishable from something an ordinary HTML form could already have sent, so the browser sends it and applies the check afterwards.
Anything else is preflighted: an OPTIONS request goes first, asking whether the method and the headers are permitted, and the real request is only sent if the answer covers them. Two everyday things trigger this and surprise people. Sending JSON puts Content-Type outside the safelist. Adding an Authorization header puts a header outside it. Either turns one round trip into two, which is what Access-Control-Max-Age exists to amortise.
Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true is not a very permissive configuration. It is a broken one. The check in the Fetch Standard fails outright when the credentials mode is include and the allowed origin is an asterisk, so an endpoint set up this way permits nothing whatsoever to a caller sending cookies, while reading as though it permits everything.
The same reversal catches the other wildcards. Under credentials, an asterisk in Allow-Headers or Allow-Methods stops being a wildcard and becomes a literal name, and an asterisk in Expose-Headers is ignored entirely, leaving the caller with the seven response headers that are always readable. A preflight that passes anonymously can fail the moment cookies are attached.
Because only one origin may appear in the header, a service allowing several has to choose per request and echo the chosen one back. That is normal and correct, and it introduces a caching problem: any shared cache in the path may store one caller’s response, allowed origin and all, and hand it to a different caller.
Vary: Origin is what prevents that, by telling every cache the response depends on the request’s Origin header. It is unnecessary only when the value is a constant. The second half of the job is choosing from a list you maintain rather than reflecting whatever arrives, because reflecting an arbitrary origin alongside credentials is the same hole as the wildcard, except that it works.
Usually the header is on the wrong response. Allow-Methods and Allow-Headers are only read on the preflight, and if OPTIONS is answered by something other than the handler you configured — a redirect, an auth layer, a framework default — the real request never happens.
No. The value holds exactly one serialized origin or a single asterisk, and a comma-separated list is rejected wholesale rather than searched for a match. An endpoint written that way allows nobody while appearing to allow several.
It limits which browser-based callers can read your responses, and that is all. It does nothing about a request from curl, a server, or a mobile app, none of which enforce any of this. Authorisation still has to happen on your side.
Only seven response headers are readable cross-origin by default: Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified and Pragma. Anything else, including Location and any custom one, reads as null until it is named in Access-Control-Expose-Headers.