CORS — Security Implications
TL;DR
CORS is not a security feature — it’s a mechanism that lets servers opt into relaxing the same-origin policy. The browser enforces SOP by default; CORS is the opt-out. Misconfigured CORS is one of the most common security vulnerabilities in modern APIs. The senior knowledge: Access-Control-Allow-Origin: * + credentials is the canonical fatal bug; echoing back arbitrary origins is the second; trusting the Origin header for authorization is the third.
(Protocol mechanics in ../18_browser_internals/09_cors_deep.md; this file is security-focused.)
Interview Q&A
Q: Is CORS a security feature?
A: No. This is the misconception interviewers test for.
- Same-Origin Policy (SOP) is the security mechanism — browsers enforce it by default to prevent
https://evil.comfrom readinghttps://bank.com’s responses. - CORS is the opt-out — a server explicitly tells the browser “this resource is safe to share with origin X.”
CORS configuration controls how relaxed SOP is. A wrong CORS config doesn’t break security — it removes security.
The threat model: malicious JavaScript on evil.com wants to read your API responses (auth-gated). SOP blocks the read by default. CORS misconfiguration lets the read through.
Q: The canonical CORS bug — Access-Control-Allow-Origin: * with credentials.
A: Browsers refuse to honor Allow-Origin: * with credentialed requests (cookies, Authorization header). The combination doesn’t even work.
But the trap: developers echo back arbitrary origins to work around this:
// Naive server
const origin = req.headers.origin;
res.setHeader("Access-Control-Allow-Origin", origin); // any origin gets echoed
res.setHeader("Access-Control-Allow-Credentials", "true");
Now https://evil.com can:
- Trick a logged-in user to visit
evil.com. - evil.com’s JS does
fetch("https://api.yoursite.com/me", { credentials: "include" }). - Browser sends cookies; server returns
Allow-Origin: https://evil.com. - evil.com reads the user’s data.
Fix: maintain an allowlist of permitted origins; only echo if the request’s origin is in the list:
const ALLOWED = ["https://app.example.com", "https://admin.example.com"];
const origin = req.headers.origin;
if (ALLOWED.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Vary", "Origin");
}
res.setHeader("Access-Control-Allow-Credentials", "true");
Vary: Origin so CDN doesn’t cache one origin’s response for another.
Q: Why is Vary: Origin critical?
A: Without it, a CDN may cache the response with Allow-Origin: https://app.example.com and serve it to a request from https://other.example.com, which would then be blocked by the browser (mismatched origin).
Worse case: CDN serves a response with Allow-Origin: * to a credentialed-context request. Now any origin can read it.
Vary: Origin tells the CDN: cache per origin. Each origin gets its correctly-tailored response.
Q: Trusting the Origin header for authorization — bad.
A: Some servers check req.headers.origin and grant access based on it:
// BAD — origin header used for auth decision
if (req.headers.origin === "https://app.example.com") {
return res.json(sensitiveData);
}
Origin is set by the browser in cross-origin contexts. For:
- Non-browser clients (curl, scripts, server-to-server):
Originis missing or anything the client wants. - Same-origin requests from a browser:
Originmay be missing (older specs). - Spoofing: a malicious server can send any
Originheader.
Origin is informational; never authorization. Real auth uses cookies/tokens, not headers the client sets.
Q: Public APIs with Access-Control-Allow-Origin: *.
A: Acceptable when:
- The API is truly public (no auth, no per-user data).
- No credentials are involved (no cookies, no
Authorizationheader). - The response contains nothing the SOP was protecting.
Examples: weather APIs, public CDN content, public chart data. The wildcard says “anyone can read.” If your data needs auth, you don’t want *.
Q: Credentials in CORS — what to know.
A:
Client side:
fetch(url, { credentials: "include" }); // send cookies cross-origin
fetch(url, { credentials: "same-origin" }); // default — only same-origin
fetch(url, { credentials: "omit" }); // never send cookies
Server side:
Access-Control-Allow-Origin: https://app.example.com # NEVER *
Access-Control-Allow-Credentials: true
Both sides must opt in. If either is missing, cookies aren’t sent or the response is blocked.
Security implication: credentialed CORS exposes more risk because cookies attach automatically. Be conservative with the origin allowlist.
Q: Subdomain CORS — wildcard origins?
A: A server with many subdomains (*.example.com):
const ALLOWED_PATTERN = /^https:\/\/[a-z0-9-]+\.example\.com$/;
if (ALLOWED_PATTERN.test(req.headers.origin)) {
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
res.setHeader("Vary", "Origin");
}
Caveat: any subdomain takeover (a dangling DNS record pointing to an unclaimed service) becomes an attacker-controlled origin in your allowlist. Audit DNS regularly.
Q: CORS + Authorization header.
A: Authorization: Bearer <token> is a non-simple header → triggers preflight. Server must allow it:
Access-Control-Allow-Headers: Authorization, Content-Type
Without this, the browser blocks the request after the preflight.
Bearer tokens (in Authorization header, set by JS) are CSRF-safe (not auto-attached), but XSS-vulnerable (XSS reads them from memory/storage). httpOnly cookies are the opposite trade-off. See 06_token_storage.md.
Q: CORS preflight cache poisoning.
A: A subtle attack: server’s preflight response varies based on something other than Origin (e.g., Cookie from a logged-in user). Browser caches the preflight; subsequent unauthenticated request hits the cached “I’m allowed” preflight.
Defense: don’t vary preflight response by anything user-specific. The preflight should be deterministic per origin + URL + method. If it must vary, set Access-Control-Max-Age: 0.
Q: Common patterns and their security implications.
A:
| Pattern | Security |
|---|---|
Allow-Origin: * + no credentials |
ok for public data |
Allow-Origin: * + credentials |
browser refuses; can’t even ship |
Allow-Origin: <echo> + credentials, no allowlist |
dangerous — any origin reads your data |
Allow-Origin: <echo from allowlist> + credentials |
safe |
Allow-Origin: <specific origin> + credentials |
safe |
Allow-Origin: null |
dangerous — null can be sent by sandboxed iframes, redirected pages |
Allow-Origin: * for ranged image (e.g., for canvas) |
dangerous if behind auth |
Q: How do you actually test CORS config?
A:
- curl from a “different origin” simulation:
Look atcurl -i -H "Origin: https://evil.com" https://api.example.com/meAccess-Control-Allow-Originin the response. - Test in a browser with a malicious-origin test page (a local file or test server).
- Burp Suite / OWASP ZAP — automated misconfig scanners.
- Continuous monitoring — if your CORS allowlist is dynamic (echoed from a DB), set up alerts for unusual origins being added.
Gotchas / edge cases
Origin: nullis sent by sandboxed iframes,file://, some redirects. Never include in your allowlist — attackers can craft requests withOrigin: null.- Preflights are not encrypted differently —
OPTIONSrequests follow same HTTPS rules. - WebSocket doesn’t honor CORS the same way — the
Originheader is sent; server must check it. Browser doesn’t auto-block based on response. - CORS doesn’t prevent CSRF — the browser may still send the request (with cookies via SameSite), it just hides the response. CSRF doesn’t need to read the response; it just needs the side effect.
Access-Control-Expose-Headers— JS can only read default-safe response headers + ones listed here. Custom headers likeX-Total-Countneed to be exposed.- DNS rebinding attacks can circumvent CORS by mapping an attacker-controlled hostname to internal IPs. Mitigate with
Hostheader validation, not just CORS.
What a senior is expected to say
- “CORS is not a security feature — Same-Origin Policy is. CORS is the server’s opt-in to relax it. Misconfiguration removes protection.”
- “Never
Allow-Origin: *with credentials — the browser refuses. The trap is echoing back arbitrary origins; always allowlist.” - “Trusting the
Originheader for authorization is wrong —Originis browser-set and can be missing or spoofed in non-browser contexts.” - “
Vary: Originso CDNs don’t cache one origin’s response for another.” - “Bearer tokens (CORS-friendly Authorization header) are CSRF-safe but XSS-vulnerable. httpOnly cookies are XSS-safe but need CSRF protection.”
- “CORS doesn’t prevent CSRF — the browser still sends the cookie; CORS only hides the response. CSRF doesn’t need the response.”
Cross-references
- CORS protocol details: ../18_browser_internals/09_cors_deep.md
- CSRF (different vector, related): 02_csrf.md
- Token storage: 06_token_storage.md
Further reading
- MDN — CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
- OWASP — Origin/CORS misconfiguration: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Request_Forgery_Prevention_Cheat_Sheet.html
- “Common CORS misconfigurations” — write-ups across security research