Skip to main content

CORS Explained: The Mental Model and the Misconfigurations That Get Exploited

CORS protects users, not your server. The right mental model, the dangerous patterns (origin reflection, wildcard with credentials) and correct configs.

The CompleteStatus Team 6 min read

CORS Explained: The Mental Model and the Misconfigurations That Get Exploited

Want this checked continuously?

CompleteStatus grades your headers, SSL and email security 24/7 — free, commercial use allowed.

Monitor your site free

CORS misconfiguration is one of the most common quiet vulnerabilities on the web, and it usually starts the same way: a developer hits a red "blocked by CORS policy" error in the console, searches for a fix, pastes the first header combination that makes the error disappear, and moves on. Sometimes that paste is harmless. Sometimes it hands every website on the internet the ability to read your users' private API responses. You can't tell which unless you know what CORS is actually for.

This post sets out that model, then walks through the misconfigurations that get exploited.

The mental model: CORS protects users, not your server

Browsers enforce the same-origin policy: JavaScript on evil.example can send a request to your-api.com (it always could), but it cannot read the response. That single rule is why being logged in to your bank in one tab is safe while browsing sketchy sites in another: the sketchy site can fire requests at the bank, with your cookies attached, but the responses come back sealed.

CORS (Cross-Origin Resource Sharing) is the mechanism for selectively relaxing that protection. Your server's CORS headers tell the browser: "responses from me may be read by pages from these origins."

Two consequences follow, and they resolve most CORS confusion:

  1. CORS is not a firewall. It does nothing against curl, server-to-server calls, or attackers hitting your API directly; they were never subject to a same-origin policy to begin with. If an endpoint must not be publicly readable, it needs authentication, not CORS.
  2. A CORS misconfiguration is an attack on your users. The victim is a logged-in user whose browser is tricked into fetching your API from a malicious page and, thanks to your overly generous headers, letting that page read the authenticated response.

In other words: your CORS policy is you deciding which other websites may act as a frontend to your API, with your users' credentials. Write it with that sentence in mind.

How the handshake works

For simple requests (GET, or POSTs with form-like content types), the browser sends the request with an Origin header and checks the response's Access-Control-Allow-Origin before releasing it to the page. For anything else (a PUT, a Content-Type: application/json, a custom header) the browser first sends a preflight OPTIONS request, and only proceeds if the server's answer permits the method, headers and origin:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400

The credential rule is the important one: if the request carries cookies or HTTP auth (credentials: 'include'), the response's Access-Control-Allow-Origin must be an exact origin, not *. The spec forbids the wildcard with credentials, and browsers enforce it. That rule exists precisely to stop "everyone may read authenticated responses." Which brings us to how people break it anyway.

The dangerous patterns

1. Origin reflection: the wildcard-with-credentials workaround

A developer wants credentialed requests to work from several origins, discovers * is rejected, and "solves" it by echoing whatever arrives:

// DO NOT do this
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');

This is the worst CORS configuration possible. Every origin on the internet is now an exact-match allowed origin, https://evil.example included. Any page a logged-in user visits can fetch() your API with credentials and read the response: profile data, API keys, messages, whatever your API serves. You've re-created the wildcard the spec forbids, but with credentials enabled. Reflection is only acceptable after validating the origin against a strict allowlist.

2. Substring and regex validation that doesn't anchor

"Validate the origin" done sloppily is reflection with extra steps:

  • Checking origin.includes('your-site.com') is defeated by your-site.com.evil.example or notyour-site.com.
  • Checking origin.startsWith('https://your-site') is defeated by registering your-sitex.com.
  • An unescaped-dot regex like /^https:\/\/app.example\.com$/ where . matches any character is defeated by appXexample.com.

Compare the Origin against an exact list of full origins (scheme, host and port). Nothing fuzzier.

3. Trusting the null origin

Sandboxed iframes and some redirect flows produce Origin: null. Allowlisting null (often added to "fix" a local-file test) is exploitable: an attacker can manufacture a null-origin request from a sandboxed iframe. Never allow it in production.

4. Wildcard on responses that were never meant to be public

Access-Control-Allow-Origin: * without credentials is safe only if the endpoint returns nothing sensitive without explicit auth headers. The trap is APIs that authorise by IP range, VPN, or network position: a wildcard turns every browser inside that network into a proxy for reading your "internal" API. If the data isn't public, don't mark it publicly readable.

5. Trusting all subdomains forever

https://*.example.com-style allowlisting means one XSS on any forgotten subdomain (a stale marketing microsite, an old staging box) becomes credentialed read access to your API. Wide subdomain trust is a risk multiplier; list the frontends that actually exist.

How to configure CORS correctly

  • Maintain an explicit allowlist of full origins (https://app.example.com) in config, compare exactly, and reflect the matched value.
  • Send Vary: Origin whenever the header depends on the request origin, so caches and CDNs don't serve one origin's CORS response to another.
  • Enable Access-Control-Allow-Credentials: true only if you actually use cookie/HTTP auth cross-origin. Token-in-header APIs usually don't need it. If you do run credentialed cross-site cookies, they must be SameSite=None; Secure; see the cookie security guide for those trade-offs.
  • Don't blanket-enable CORS server-wide. Scope it to the API routes that need it; your HTML pages and admin panel almost never should be cross-origin readable.
  • Prefer your framework's CORS middleware (Laravel's config/cors.php, Express's cors package with an origin array) over hand-rolled header code. The hand-rolled versions are where reflection bugs live.

A minimal correct Express example:

const allowed = ['https://app.example.com', 'https://admin.example.com'];
app.use(cors({
  origin: allowed,          // exact-match allowlist, not a function that echoes
  credentials: true,
}));

Misconfigurations are silent, so check for them

A dangerous CORS policy throws no errors, breaks no tests, and looks identical to a correct one in the browser. Everything works, which is the problem. The quickest manual test for reflection is to send a request with a hostile origin and see what comes back:

curl -s -I -H "Origin: https://evil.example" https://your-api.com/v1/me | grep -i access-control

If the response echoes https://evil.example alongside Access-Control-Allow-Credentials: true, you have pattern 1. Run it against each API route family after any change to your CORS middleware.

For the rest, CompleteStatus's continuous monitoring grades your security headers A+ to F: CSP, cookie flags, clickjacking protection and more, plus a critical finding for the wildcard-with-credentials combination on the responses it fetches. The whole header set is covered in our complete security-headers guide. If a deploy drops the grade below B, you get an alert.

Get your grade in seconds with the free security score checker or inspect the raw headers with the security header checker, then start monitoring free to make sure a quick CORS "fix" never ships unnoticed. The free tier allows commercial use.

Written with AI assistance and reviewed by the CompleteStatus team.