Free Handy Tools

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.

The request you are asking about

The Origin header the browser sends. Scheme, host and port, nothing else.

Anything outside GET, HEAD and POST earns a preflight.

Comma separated, for example authorization, x-api-key.

application/json is the value that turns a plain POST into a preflighted one.

What the browser would do

Paste the response headers above and the answer for this request appears here.

Findings

  • GoodNo cross-origin read is permitted 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

The safelists a preflight depends on

Methods that never preflight

GET, HEAD, POST

Request headers a script may set freely

accept, accept-language, content-language, content-type, range

Content-Type values on the safelist

application/x-www-form-urlencoded, multipart/form-data, text/plain

Response headers readable without being exposed

cache-control, content-language, content-length, content-type, expires, last-modified, pragma

Header reference

  • 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 protocol
  • Access-Control-Allow-CredentialsSet to true, permits the response to be read by a request that carried cookies or TLS client certificates.Fetch Standard, CORS protocol
  • Access-Control-Allow-MethodsAnswers a preflight with the methods permitted. Ignored on any response that is not a preflight.Fetch Standard, CORS-preflight fetch
  • Access-Control-Allow-HeadersAnswers a preflight with the request headers permitted. Also only read on a preflight response.Fetch Standard, CORS-preflight fetch
  • Access-Control-Expose-HeadersNames response headers the caller may read beyond the seven that are always readable.Fetch Standard, CORS-safelisted response-header name
  • Access-Control-Max-AgeHow many seconds a preflight result may be cached. Browsers impose their own ceiling on it.Fetch Standard, CORS-preflight cache
  • VaryTells caches the response depends on a request header. Required in practice whenever the allowed origin is echoed back.RFC 9110 § 12.5.5

Because 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.

Permission to read, never permission to send

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.

The requests that ask permission first

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.

The wildcard that cannot carry credentials

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.

Echoing an origin without poisoning a cache

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.

Preflights, wildcards, and headers curl can see

Why does the browser say the header is missing when curl shows it?

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.

Can I list several origins in one header?

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.

Does tightening these headers protect my API?

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.

Why can my script not read a header that is plainly there?

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.

Does a CORS error mean the request failed?

No. The request usually reached your server and did whatever it does; the browser withheld the reply from the calling script afterwards. Check the server log before assuming nothing happened, because a cross-origin POST that reports a CORS error may still have written the row.

Why is my Origin header null?

Because the request came from an opaque origin: a page opened from a file path, a sandboxed iframe, or a redirect that stripped it. Allowing the literal value null is not a fix — any page can produce it, so it hands access to every sandboxed document there is.