Nearly every developer meets CORS the same way: a request that works fine in Postman fails in the browser with a red console message about Access-Control-Allow-Origin. The instinct is to make the error go away. That instinct is how insecure APIs get shipped.

Check what headers an endpoint returns with the CORS Checker.

What CORS is actually protecting

The rule underneath it all is the same-origin policy: a page on one origin cannot read the response from another origin. Without it, any site you visited could quietly make requests to your bank in your browser, with your cookies attached, and read the results.

CORS is the controlled exception. It lets a server say "this specific other origin is allowed to read my responses". The important word is read — in most cases the request is still sent; the browser just refuses to hand the response to the calling page.

That is why the error appears in the browser and not in Postman. Postman is not a browser and has no same-origin policy to enforce. The API was never the thing blocking you.

The headers

Access-Control-Allow-Origin — which origin may read the response. Either one specific origin, or * for any.

Access-Control-Allow-Credentials — whether cookies and auth headers may be sent. If this is true, the browser refuses to accept * as the origin. That restriction exists for a good reason.

Access-Control-Allow-Methods — which HTTP methods are permitted on a preflighted request.

Access-Control-Allow-Headers — which request headers the client may send. If you send Authorization or a custom header, it must be listed here.

Access-Control-Max-Age — how long the browser may cache the preflight result. Worth setting; otherwise every request pays for an extra round trip.

Preflight, and why you suddenly have two requests

Simple requests go straight out. Anything else triggers a preflight: the browser first sends an OPTIONS request asking whether the real request is allowed.

A request is "simple" only if it is GET, HEAD or POST, and its Content-Type is one of application/x-www-form-urlencoded, multipart/form-data or text/plain, and it carries no unusual headers.

Which means the moment you send JSON — Content-Type: application/json — you are preflighted. That single detail explains most confused "why are there two requests" questions. If your server does not handle OPTIONS, the real request never happens.

The three dangerous mistakes

1. Reflecting the Origin header

The most common bad fix. The server reads whatever Origin the request carried and echoes it back:

Access-Control-Allow-Origin: <whatever the client sent>
Access-Control-Allow-Credentials: true

This makes the error disappear and effectively disables the same-origin policy for your API. Any site the victim visits can now make credentialed requests and read the responses. If you are going to allow specific origins, check the incoming origin against an allowlist and echo it only on a match.

2. Wildcard with credentials

Browsers block * combined with credentials, so people work around it by reflecting the origin instead — which is mistake one. If you genuinely need credentials, you need a real allowlist. There is no shortcut here, and the shortcut people reach for is the vulnerability.

3. Trusting sloppy origin matching

Allowlists implemented with startsWith or a loose regex get bypassed. A check for "origin contains example.com" happily accepts example.com.attacker.net. Compare the full origin string exactly.

What CORS is not

CORS is not authentication and not authorisation. A permissive CORS policy on an endpoint that requires a valid token is not automatically a vulnerability — and a restrictive policy on an endpoint with no auth at all protects nothing, because anything that is not a browser ignores CORS entirely.

Server-to-server requests, scripts, and command-line tools are unaffected. CORS is a browser rule. Your real access control has to live on the server.

Checking an endpoint

  1. Run the URL through the CORS Checker.
  2. If Access-Control-Allow-Origin comes back matching whatever origin was sent, that is origin reflection — investigate.
  3. Check whether Allow-Credentials: true appears alongside a permissive origin. That combination is the one that matters.
  4. Confirm OPTIONS is handled if clients send JSON.
  5. Check Allow-Methods is not just * out of convenience.

Common questions

Why does it work in Postman but not the browser?
Postman does not enforce the same-origin policy. The API is responding fine; the browser is withholding the response from your page.

Why are two requests being sent?
Your request is preflighted — usually because of Content-Type: application/json. The OPTIONS request asks permission first.

Is Access-Control-Allow-Origin: * insecure?
Not by itself, on a public endpoint with no credentials. It becomes serious when combined with cookies or auth headers.

Does CORS protect my API?
No. It restricts browser pages. Anything that is not a browser ignores it. Authentication and authorisation are separate work.

Where to go next

  • Test an endpoint with the CORS Checker.
  • If the API uses tokens, check what is inside them with the JWT Decoder.
  • A loose CORS policy can undo a careful Content-Security-Policy — review them together.
  • Unsure whether a policy you have found is exploitable? Ask in the BitCops community.
  • Still blocked after changing the headers? Paste the request and the response headers in the BitCops forum — CORS failures are almost always a detail nobody spots alone.