CORS Explained: Safe Cross-Origin Configuration for Modern Sites
CORS is misunderstood as often as it is misconfigured. Here is what the headers actually do, the wildcard-plus-credentials trap, and how to lock an API down correctly.
Cross-Origin Resource Sharing is where a lot of teams first meet browser security — usually because something broke and the quickest search result said to "just allow all origins." That fix works and quietly becomes a vulnerability. This is what CORS is really doing, and how to configure it without opening your API to every site on the internet.
What CORS actually controls
CORS is not a server-side access control. It is a browser mechanism that decides whether JavaScript running on origin A is allowed to *read* a response from origin B. It does nothing to stop a direct request from curl, a server, or a script that ignores the browser. Treat it as a guardrail for browser clients, and keep real authorization on the server regardless.
The wildcard-plus-credentials trap
The single most dangerous CORS configuration is reflecting the request origin while also allowing credentials:
Access-Control-Allow-Origin: https://evil.example
Access-Control-Allow-Credentials: trueIf your server echoes back whatever Origin it receives and sets Allow-Credentials: true, any site can make authenticated requests as your logged-in user and read the response. The browser forbids literal * together with credentials for this reason — but reflecting the origin bypasses that safety and recreates the hole.
A correct, restrictive configuration
Match the incoming origin against an explicit allow-list and only then echo it back:
const allowed = new Set([
'https://app.example.com',
'https://admin.example.com',
]);
const origin = req.headers.get('origin');
if (origin && allowed.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
res.setHeader('Access-Control-Allow-Credentials', 'true');
}The Vary: Origin header is easy to forget and important: without it, a cache can serve one origin's allow header to a different origin.
Scope methods and headers too
Access-Control-Allow-Origin is only half the policy. Keep the rest tight:
- Allow only the methods the endpoint uses, e.g.
Access-Control-Allow-Methods: GET, POST. - Allow only the request headers you expect, not a blanket list.
- Set a sensible
Access-Control-Max-Ageso preflight requests are cached. - Never combine
Allow-Origin: *withAllow-Credentials: true.
When a wildcard is actually fine
A truly public, unauthenticated, read-only API — a currency rate feed, a public status endpoint — can use Access-Control-Allow-Origin: * safely, precisely because there are no credentials and nothing private to leak. The danger is only ever *-style openness combined with authentication.
Test it from a real cross-origin context
CORS behaves differently for simple requests and for preflighted ones, so test both from a browser on a different origin, not just with curl (which ignores CORS entirely). A scan checks your CORS response headers automatically alongside the rest of your security checks; to see how CORS findings fit into a prioritized fix list, read How to Read a Website Security Scan Report.
Turn these checks into a scan report for your own site.
Run a free scan and get prioritized, actionable findings in about 30 seconds.
Scan my siteKeep reading
How to Read a Website Security Scan Report
A scan returns dozens of findings and a score. Here is how to turn that output into a short, prioritized fix list without drowning in noise.
The 10 Most Common Security Header Mistakes (and How to Fix Them)
Security headers are the cheapest hardening you can ship — and the easiest to get subtly wrong. Here are the ten mistakes scanners flag most, with the exact fix for each.