1---2name: cors3description: Configure Cross-Origin Resource Sharing correctly to avoid security issues and debugging pain.4---5
6## Preflight Triggers
7
8- Any header except: Accept, Accept-Language, Content-Language, Content-Type (with restrictions)
9- Content-Type other than: application/x-www-form-urlencoded, multipart/form-data, text/plain
10- Methods: PUT, DELETE, PATCH, or any custom method
11- ReadableStream in request body
12- Event listeners on XMLHttpRequest.upload
13- One trigger = preflight; simple requests skip OPTIONS entirely
14
15## Credentials Mode
16
17- `Access-Control-Allow-Origin: *` incompatible with credentials—must specify exact origin
18- `Access-Control-Allow-Credentials: true` required for cookies/auth headers
19- Fetch: `credentials: 'include'`; XHR: `withCredentials = true`
20- Without credentials mode, cookies not sent even to same origin for cross-origin requests
21
22## Wildcard Limitations
23
24- `*` doesn't match subdomains—`*.example.com` is invalid, not a pattern
25- Can't use `*` with credentials—specify origin dynamically from request
26- `Access-Control-Allow-Headers: *` works in most browsers but not all—list explicitly for compatibility
27- `Access-Control-Expose-Headers: *` same issue—list headers you need to expose
28
29## Origin Validation
30
31- Check Origin header against allowlist—don't reflect blindly (security risk)
32- Regex matching pitfall: `example.com` matches `evilexample.com`—anchor the pattern
33- `null` origin: sandboxed iframes, file:// URLs—usually reject, never allow as trusted
34- Missing Origin header: same-origin or non-browser client—handle explicitly
35
36## Vary Header (Critical)
37
38- Always include `Vary: Origin` when response depends on origin—even if you allow only one
39- Without Vary: CDN/proxy caches response for one origin, serves to others—breaks CORS
40- Add `Vary: Access-Control-Request-Headers, Access-Control-Request-Method` for preflight caching correctness
41
42## Exposed Headers
43
44- By default, JS can only read: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
45- Custom headers invisible to JS unless listed in `Access-Control-Expose-Headers`
46- `X-Request-ID`, `X-RateLimit-*`, etc. need explicit exposure—common oversight
47
48## Preflight Caching
49
50- `Access-Control-Max-Age: 86400` caches preflight for 24h—reduces OPTIONS traffic significantly
51- Chrome caps at 2 hours; Firefox at 24 hours—values above are silently reduced
52- Cached per origin + URL + request characteristics—not globally
53- Set to 0 or omit during development—caching hides config changes
54
55## Debugging
56
57- CORS error in browser = request reached server and came back—check server logs
58- Preflight failure: server must return 2xx with CORS headers on OPTIONS—404/500 = failure
59- Opaque response in fetch: `mode: 'no-cors'` succeeds but response is empty—usually not what you want
60- Network tab shows CORS errors; Console shows which header is missing
61
62## Common Server Mistakes
63
64- Only setting CORS headers on main handler, not OPTIONS—preflight fails
65- Setting headers after error response—CORS headers missing on 4xx/5xx breaks error handling
66- Proxy stripping headers—verify headers reach client, not just that server sets them
67- `Access-Control-Allow-Origin: "*", "https://example.com"`—must be single value, not list
68
69## Security
70
71- Don't reflect Origin header blindly—validate against allowlist first
72- Private Network Access: Chrome requires `Access-Control-Allow-Private-Network: true` for localhost access from public web
73- CORS doesn't prevent request from being sent—just blocks response reading; server still processes it
74- Sensitive endpoints: don't rely on CORS alone; use authentication + CSRF tokens