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')matchesevilexample.com. CheckingstartsWith('https://example.com')matcheshttps://example.com.attacker.example. Compare the full origin string. null. Sandboxed iframes and some redirects sendOrigin: null. Allowlistingnullmeans any attacker can obtain it from a sandboxed frame. Never include it.