frontend / security / 04_cors_security_angle.md

CORS — Security Implications

6 min read source

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.com from reading https://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:

  1. Trick a logged-in user to visit evil.com.
  2. evil.com’s JS does fetch("https://api.yoursite.com/me", { credentials: "include" }).
  3. Browser sends cookies; server returns Allow-Origin: https://evil.com.
  4. 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): Origin is missing or anything the client wants.
  • Same-origin requests from a browser: Origin may be missing (older specs).
  • Spoofing: a malicious server can send any Origin header.

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 Authorization header).
  • 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 dangerousnull 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:

  1. curl from a “different origin” simulation:
    curl -i -H "Origin: https://evil.com" https://api.example.com/me
    Look at Access-Control-Allow-Origin in the response.
  2. Test in a browser with a malicious-origin test page (a local file or test server).
  3. Burp Suite / OWASP ZAP — automated misconfig scanners.
  4. Continuous monitoring — if your CORS allowlist is dynamic (echoed from a DB), set up alerts for unusual origins being added.

Gotchas / edge cases

  • Origin: null is sent by sandboxed iframes, file://, some redirects. Never include in your allowlist — attackers can craft requests with Origin: null.
  • Preflights are not encrypted differentlyOPTIONS requests follow same HTTPS rules.
  • WebSocket doesn’t honor CORS the same way — the Origin header 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 like X-Total-Count need to be exposed.
  • DNS rebinding attacks can circumvent CORS by mapping an attacker-controlled hostname to internal IPs. Mitigate with Host header 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 Origin header for authorization is wrong — Origin is browser-set and can be missing or spoofed in non-browser contexts.”
  • Vary: Origin so 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

Further reading