Blog / CORS Explained — the Mental Model, and the Misconfigurations That Actually Get Exploited
cors security-headers api-security

CORS Explained — the Mental Model, and the Misconfigurations That Actually 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 Actually 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 quietly common 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. The difference is impossible to see unless you have the right mental model of what CORS is actually for.

So let's build that model, then walk through the misconfigurations that actually 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 never had 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 load-bearing 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') — defeated by your-site.com.evil.example or notyour-site.com.
  • Checking origin.startsWith('https://your-site') — defeated by registering your-sitex.com.
  • An unescaped-dot regex like /^https:\/\/app.example\.com$/ where . matches any character — 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. And 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 watch 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 exactly the problem. Like the rest of your security posture, it only stays correct if something is checking. CompleteStatus's continuous monitoring grades your security posture A+ to F — headers, CSP, cookie flags, CORS and more, with the whole stack covered in our complete security-headers guide — and alerts you the moment a deploy weakens it.

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.

Stop checking by hand

CompleteStatus runs these exact checks around the clock — uptime, SSL, DNS, security headers and SPF/DKIM/DMARC — and alerts you the moment something changes. One dashboard, one bill.
Start free Run a free check
Uptime is table stakes. We watch the rest — security headers, email authentication, certs and DNS, with the fix attached.
Start free
Company
© 2026 CompleteStatus. All rights reserved. CompleteStatus — operated in the United States · support@completestatus.com