← All posts

CORS Is Not a Firewall: The API Lockdown That Didn't Lock Anything

2022-03-02 · security · api · web

This one still makes me wince, which is why it’s worth writing down.

The API backed a React SPA. We wanted “only our frontend can call the API,” so we configured CORS: Access-Control-Allow-Origin: https://app.example.com, allowlist of methods, done. It felt airtight. The browser enforced it — we tested a rogue origin and got blocked. Ship it.

The demo that ended the illusion

Someone on the team — correctly skeptical — opened a terminal on a completely unrelated machine:

curl -i https://api.example.com/api/v1/users/me \
  -H "Authorization: Bearer <any-valid-token>"

Full 200 response. Every endpoint. No origin header, no preflight, no problem.

What CORS actually is

CORS is a browser cooperation mechanism. It stops a malicious web page in a victim’s browser from reading responses from your API using the victim’s credentials. That’s it. It says nothing at all about:

CORS is a fence around the browser’s garden. It is not a wall around your API.

What we should have done (and then did)

  1. Authentication on every route — no anonymous catch-alls “temporarily” left open
  2. Authorization per resource — checking that the token’s owner may touch that object, not just that a token exists
  3. Rate limiting — because if an attacker can hammer the endpoint, you have a different problem than origin control
  4. CORS kept tight anyway — defense in depth for the browser-borne attack surface it actually covers

The allowlist stayed. But nobody on the team ever again described it as “locking down” anything.

The general lesson

Every security control has a precise threat model. When you can’t state in one sentence which attacker, doing what, is stopped by this, you don’t have a control — you have a comfort blanket. CORS’s sentence is: “stops other websites from reading your API responses in a user’s browser.” Say it out loud before you rely on it for anything else.