Skip to content
IsSafeSite
2 min readIsSafeSite Team

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.

CORSAPIsHardening

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: true

If 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-Age so preflight requests are cached.
  • Never combine Allow-Origin: * with Allow-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 site

Keep reading