HeaderAudit

CORS: the one header where setting it is the risk

Every other header on this site is about what you are missing. CORS is the opposite: the danger is in setting it, and specifically in the pattern most people reach for to make a CORS error go away.

What the same-origin policy was doing for you

By default a script on attacker.example may send a request to your API — including the user's cookies — but may not read the response. That read barrier is what stops any site on the internet from quietly pulling a logged-in user's data out of your API.

CORS exists to relax that barrier deliberately. Relaxing it accidentally hands over exactly what it was protecting.

The dangerous pattern

Access-Control-Allow-Origin: <echo of the Origin request header> Access-Control-Allow-Credentials: true

This is what you get when you fix CORS errors by reflecting whatever origin asked. The result: any website can make authenticated requests as your logged-in user and read the responses. It is a full account-data disclosure, reachable by anyone who can get a victim to load a page.

Browsers refuse to combine a literal * with credentials, which is why the reflection pattern exists — it looks like it satisfies the browser while actually being far more permissive than the wildcard would have been.

Doing it correctly

Match the request origin against an explicit allowlist and echo it only on a match:

const ALLOWED = new Set(['https://app.example.com', 'https://admin.example.com']); const origin = request.headers.get('Origin'); if (ALLOWED.has(origin)) { response.headers.set('Access-Control-Allow-Origin', origin); response.headers.set('Access-Control-Allow-Credentials', 'true'); response.headers.set('Vary', 'Origin'); }

Vary: Origin is not optional. Without it a cache can serve a response containing one origin's ACAO header to a request from a different origin.

Two traps in allowlist matching

  • Prefix and suffix matching. Checking origin.endsWith('example.com') matches evilexample.com. Checking startsWith('https://example.com') matches https://example.com.attacker.example. Compare the full origin string.
  • null. Sandboxed iframes and some redirects send Origin: null. Allowlisting null means any attacker can obtain it from a sandboxed frame. Never include it.
A wildcard on genuinely public, unauthenticated data — a fonts CDN, a public price feed — is fine. The rule is not "never use CORS", it is "never reflect the origin on anything that returns user data".

Check a site