ThatAIguyA world of ideas

THE CONSCIOUSNESS ATLAS
Read. Experiment. Follow the evidence.

ConsciousnessLearn · Explore · Joseph Anady

Janady13/crawler-signal-reference / framework-http-cors-headers.md · Lines 1501–2069

Source identity, revision, and recovered contents.

GitHub sourceSource text recovered

A continuation of Janady13/crawler-signal-reference / framework-http-cors-headers.md. The complete source identity, every recorded occurrence, and linked equations are on its first page.

Source transcript · continued

Download complete source text ↓2,069 lines · 82.1 KB
listen 443 quic; http2 on; http3 on; server_name api.example.com; ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_early_data on; # HTTP/3 advertisement (from framework-http-performance-headers.md) add_header Alt-Svc 'h3=":443"; ma=86400' always; # Security baseline (from framework-http-security-headers.md) include snippets/common-security-headers.conf; # API location with full CORS handling location / { # Preflight (OPTIONS) handler if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-CSRF-Token, X-Request-ID" always; add_header Access-Control-Allow-Credentials "true" always; add_header Access-Control-Max-Age "86400" always; add_header Vary "Origin" always; add_header Cache-Control "public, max-age=86400" always; return 204; } # Actual request handling: CORS headers plus pass to upstream if ($cors_origin) { add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Credentials "true" always; add_header Access-Control-Expose-Headers "X-Total-Count, X-RateLimit-Remaining, Server-Timing, ETag, Link" always; add_header Vary "Origin" always; } # SEO: API endpoints should not be indexed (from framework-http-seo-headers.md) add_header X-Robots-Tag "noindex, nofollow" always; # Cache control (from framework-http-caching-headers.md) add_header Cache-Control "public, max-age=0, must-revalidate" always; proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass_header Server-Timing; } } ``` After deploying: ```bash nginx -t && systemctl reload nginx ``` Verify the full flow: ```bash # Preflight curl -sI -X OPTIONS -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: PUT" \ -H "Access-Control-Request-Headers: authorization" \ https://api.example.com/users/123 # Actual curl -sI -X PUT -H "Origin: https://app.example.com" \ -H "Authorization: Bearer test" \ https://api.example.com/users/123 ``` Both should return the expected CORS headers. --- ## 15. AUDIT CHECKLIST Run through these 50 items for any production CORS configuration. ### Access-Control-Allow-Origin 1. [ ] Header present on every CORS enabled endpoint (both preflight and actual response). 2. [ ] Header value is specific (not `*`) for any endpoint with credentials. 3. [ ] Multi origin support uses map based validation, not raw `$http_origin` reflection. 4. [ ] No origin reflection without validation against an allow list. 5. [ ] Allow list regex matches exactly (anchored with `^` and `$`). 6. [ ] Vary: Origin header set whenever Allow-Origin is dynamic. 7. [ ] Origins listed include exact scheme (https://, not just example.com). 8. [ ] Origins listed include port when non standard. ### Access-Control-Allow-Methods 9. [ ] Preflight response includes the methods the API actually accepts. 10. [ ] OPTIONS is always in the list (some browsers require it). 11. [ ] No methods listed that the API does not accept (avoid misleading the browser). 12. [ ] PUT, DELETE, PATCH explicitly listed if the API uses them. ### Access-Control-Allow-Headers 13. [ ] All custom request headers used by the frontend are listed. 14. [ ] Content-Type is listed (required for JSON APIs). 15. [ ] Authorization is listed (required for token based auth). 16. [ ] X-CSRF-Token (or equivalent) is listed if used. 17. [ ] Header names match the case used by the frontend (case insensitive but spelled correctly). ### Access-Control-Expose-Headers 18. [ ] Custom response headers used by the frontend are explicitly exposed. 19. [ ] X-Total-Count or pagination headers exposed if pagination is used. 20. [ ] Rate limit headers (X-RateLimit-*) exposed if rate limiting is announced. 21. [ ] Server-Timing exposed if RUM relies on it for cross origin endpoints. 22. [ ] ETag exposed if client side validation uses it. ### Access-Control-Max-Age 23. [ ] Max-Age header present on every preflight response. 24. [ ] Value is at least 7200 (Chrome cap) to maximize cross browser benefit. 25. [ ] Value 86400 sent to take full advantage of Firefox cap (Chrome still caps at 7200). 26. [ ] No accidentally setting Max-Age to 0 or -1 in production. ### Access-Control-Allow-Credentials 27. [ ] Set to `true` only on endpoints that require authenticated cross origin access. 28. [ ] When set, Allow-Origin is specific (not wildcard). 29. [ ] When set, all other Allow-* and Expose-Headers use specific values, not wildcards. 30. [ ] Cookies used for auth have SameSite=None; Secure attributes. 31. [ ] Frontend uses `credentials: "include"` in fetch when needed. ### Preflight handling 32. [ ] OPTIONS requests return 204 No Content (or 200). 33. [ ] Preflight response includes all required CORS headers. 34. [ ] The `if ($request_method = OPTIONS)` pattern is used safely (with `return`, not `proxy_pass`). 35. [ ] Preflight is handled at the nginx layer (faster) when possible, not always forwarded to upstream. ### Configuration hygiene 36. [ ] No duplicate Allow-Origin headers (would indicate nginx and upstream both adding). 37. [ ] `add_header` inheritance trap not triggered (use snippet pattern from framework-http-security-headers.md). 38. [ ] All add_header lines use `always` keyword. 39. [ ] No CORS headers leaking on non CORS endpoints (e.g. main HTML pages). 40. [ ] No origins in the allow list that no longer exist or have been deprecated. ### Security 41. [ ] No `Access-Control-Allow-Origin: *` with credentials anywhere. 42. [ ] No `Access-Control-Allow-Origin: null` unless specifically required. 43. [ ] Origin allow list does not include unintended subdomains via overly broad regex. 44. [ ] Pen test does not report origin reflection vulnerability. 45. [ ] StackHawk or similar scanner reports no CORS misconfigurations. ### Cross cutting 46. [ ] `nginx -t` passes without warnings. 47. [ ] `nginx -T` shows expected CORS directives. 48. [ ] curl preflight test returns expected headers. 49. [ ] curl actual request test returns expected headers. 50. [ ] Browser console shows no CORS errors on production traffic. A site that passes all 50 has correctly configured CORS for security and performance. --- ## 16. COMMON PITFALLS Fifteen patterns to recognize and avoid. **Pitfall 1: Wildcard plus credentials.** Symptom: browser console "Access-Control-Allow-Origin cannot be wildcard when credentials are included". Why it breaks: browser explicitly refuses the combination. Fix: replace `*` with specific origin via map based validation. **Pitfall 2: Origin reflection without validation.** Symptom: scanner reports CORS misconfiguration; potential credential theft from any origin. Why it breaks: `add_header Access-Control-Allow-Origin $http_origin always;` says "whoever you are, you may read with credentials". Attacker hosts JavaScript on evil.com, makes authenticated request to victim's browser, reads response. Fix: use map based allow list (Section 5.3 and 5.5). Never reflect without validation. **Pitfall 3: Missing Vary: Origin with dynamic Allow-Origin.** Symptom: cache (CDN, proxy, browser) serves response approved for origin A to a request from origin B. Why it breaks: cache key does not include Origin; one stored response serves all requesters. Fix: always add `Vary: Origin` when Allow-Origin is dynamic. **Pitfall 4: Missing preflight handler returning 405.** Symptom: OPTIONS request returns 405 Method Not Allowed. Why it breaks: location does not handle OPTIONS, falls through to default handler. Fix: add `if ($request_method = OPTIONS) { return 204; }` block with CORS headers. **Pitfall 5: Preflight returns 200 with body instead of 204.** Symptom: works but logs are noisy, some scanners flag the unnecessary body. Why it breaks: 200 is allowed by spec; 204 is preferred and cleaner. Fix: use `return 204;` instead of letting nginx fall through to a default 200 response. **Pitfall 6: Custom request header missing from Allow-Headers.** Symptom: "Request header field X-Custom is not allowed". Why it breaks: every custom header must be in the allow list. Fix: add the header to Access-Control-Allow-Headers. **Pitfall 7: Custom response header invisible to JavaScript.** Symptom: `response.headers.get("X-Total-Count")` returns null despite the header being on the response. Why it breaks: only safelisted response headers are visible by default; everything else requires opt in. Fix: add the header to Access-Control-Expose-Headers. **Pitfall 8: Missing Max-Age, every request preflights.** Symptom: SPA feels slow on cross origin API calls. Why it breaks: default cache is 5 seconds; every API call doubles latency from preflight. Fix: add `Access-Control-Max-Age 86400`. **Pitfall 9: nginx and upstream both adding CORS headers.** Symptom: duplicate Allow-Origin header; browser rejects. Why it breaks: nginx adds one, FastAPI middleware adds another. Browser sees two values, treats as invalid. Fix: pick one layer to handle CORS. Either nginx or upstream, not both. **Pitfall 10: `if ($request_method = OPTIONS)` combined with proxy_pass.** Symptom: unexpected behavior, preflight succeeds but actual request fails. Why it breaks: `if` plus `proxy_pass` is one of the nginx if antipatterns. The combination produces non obvious behavior. Fix: keep `if` with `return` only. Use separate handling for the actual request. **Pitfall 11: Forgotten OPTIONS in Allow-Methods.** Symptom: preflight succeeds but some browsers (Safari) refuse the actual request. Why it breaks: OPTIONS should be listed even though it is the preflight method itself. Fix: include OPTIONS in the methods list. **Pitfall 12: Cookie SameSite=None missing Secure.** Symptom: cross origin auth works on https but fails on http; mobile users with mixed networks see intermittent auth failures. Why it breaks: SameSite=None requires Secure attribute, which requires HTTPS. Fix: ensure cookies are set with `Secure` attribute and site is HTTPS only (which Bubbles already is via HSTS). **Pitfall 13: Overly broad regex allows unintended subdomains.** Symptom: scanner reports `https://attacker.example.com.evil.com` is allowed. Why it breaks: regex `~^https://.*\.example\.com$` matches `attacker.example.com.evil.com` because `.` matches anything. Fix: anchor the end properly and escape dots: `~^https://[a-z0-9-]+\.example\.com$`. **Pitfall 14: Allow list missing development origins.** Symptom: works in production, breaks in local development. Why it breaks: localhost or 127.0.0.1 not in the allow list. Fix: add development origins conditionally: ```nginx map $http_origin $cors_origin { default ""; "~^https://app\.example\.com$" "$http_origin"; # Development: comment out in production if security concern "~^https?://localhost(:[0-9]+)?$" "$http_origin"; } ``` **Pitfall 15: Set-Cookie expected to be readable from JavaScript.** Symptom: JavaScript cannot read the auth cookie. Why it breaks: Set-Cookie is intentionally never exposed to script regardless of CORS. This is a fundamental browser security boundary. Fix: do not attempt to read Set-Cookie from script. Use HttpOnly cookies for sessions and let the browser handle them automatically with `credentials: "include"`. --- ## 17. DIAGNOSTIC COMMANDS Reference of every command useful for CORS investigation. ### Simulate preflight ```bash # Simulate full preflight curl -sI -X OPTIONS \ -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: PUT" \ -H "Access-Control-Request-Headers: authorization, content-type" \ https://api.example.com/users/123 # Look for the response headers curl -sI -X OPTIONS \ -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: POST" \ https://api.example.com/data | grep -iE "^(access-control|vary):" # Test from different origins for origin in "https://app.example.com" "https://evil.com" "https://localhost:5173"; do echo "=== Origin: $origin ===" curl -sI -X OPTIONS -H "Origin: $origin" \ -H "Access-Control-Request-Method: POST" \ https://api.example.com/data | grep -i access-control-allow-origin done ``` ### Simulate actual request ```bash # Actual GET with origin curl -sI -H "Origin: https://app.example.com" \ https://api.example.com/data | grep -iE "^(access-control|vary):" # Actual POST with auth curl -sI -X POST -H "Origin: https://app.example.com" \ -H "Authorization: Bearer test" \ -H "Content-Type: application/json" \ https://api.example.com/users \ -d '{"name":"test"}' # Confirm Expose-Headers and the actual headers being exposed curl -sI -H "Origin: https://app.example.com" \ https://api.example.com/users | grep -iE "^(access-control-expose|x-total|server-timing|etag):" ``` ### Test the full flow ```bash # Bash function: test a full CORS flow test_cors() { local origin=$1 local method=$2 local url=$3 echo "=== Preflight: $method $url from $origin ===" curl -sI -X OPTIONS -H "Origin: $origin" \ -H "Access-Control-Request-Method: $method" \ -H "Access-Control-Request-Headers: authorization, content-type" \ "$url" | grep -iE "^(HTTP|access-control|vary):" echo "" echo "=== Actual: $method $url from $origin ===" curl -sI -X "$method" -H "Origin: $origin" \ -H "Authorization: Bearer test" \ "$url" | grep -iE "^(HTTP|access-control|vary):" } test_cors "https://app.example.com" "PUT" "https://api.example.com/users/123" test_cors "https://evil.com" "PUT" "https://api.example.com/users/123" ``` ### Browser DevTools quick reference In Chrome DevTools: 1. **Network tab**: shows preflight (OPTIONS) and actual requests as separate entries. 2. Click the OPTIONS request: Headers tab shows the CORS response. 3. Click the actual request: Headers tab shows CORS response and any errors. 4. **Console tab**: CORS errors appear in red with the specific reason. Useful console commands: ```javascript // Test a CORS request and read response fetch("https://api.example.com/data", { method: "GET", credentials: "include", headers: {"Authorization": "Bearer test"} }) .then(r => { console.log("Status:", r.status); console.log("X-Total-Count:", r.headers.get("X-Total-Count")); console.log("Server-Timing:", r.headers.get("Server-Timing")); return r.json(); }) .then(data => console.log("Data:", data)) .catch(e => console.error("Error:", e)); // List what headers are visible fetch("https://api.example.com/data") .then(r => { for (const [k, v] of r.headers.entries()) { console.log(k, ":", v); } }); ``` ### Server side investigation ```bash # Find CORS directives in active config nginx -T 2>/dev/null | grep -iE "access-control|http_origin|cors_origin" # Check map definitions nginx -T 2>/dev/null | grep -A 10 "map \$http_origin" # Find any wildcard plus credentials combinations (security check) nginx -T 2>/dev/null | awk '/Access-Control-Allow-Origin.*\*/{f=1} /Access-Control-Allow-Credentials.*true/{if(f)print "DANGER: wildcard + credentials in same scope"} /server {/{f=0}' # Audit for any raw $http_origin usage (unvalidated reflection) nginx -T 2>/dev/null | grep "Access-Control-Allow-Origin.*\$http_origin" # Any output here is potentially a vulnerability # Apply changes nginx -t && systemctl reload nginx ``` ### External CORS testers ```bash # Online testers echo "Visit: https://cors-test.codehappy.dev/" echo "Visit: https://cors-anywhere.herokuapp.com/" # Self hosted with a test page cat > /tmp/cors-test.html << 'EOF' <!DOCTYPE html> <html><body> <h1>CORS Test</h1> <button onclick="testCors()">Test</button> <pre id="out"></pre> <script> async function testCors() { const out = document.getElementById("out"); out.textContent = "Testing..."; try { const r = await fetch("https://api.example.com/data", { method: "GET", credentials: "include", headers: {"Authorization": "Bearer test"} }); out.textContent = `Status: ${r.status}\n`; for (const [k, v] of r.headers.entries()) { out.textContent += `${k}: ${v}\n`; } } catch (e) { out.textContent = `Error: ${e.message}`; } } </script> </body></html> EOF # Serve from a different origin to test cross origin behavior python3 -m http.server -d /tmp 8080 # Visit http://localhost:8080/cors-test.html in browser ``` --- ## 18. CROSS-REFERENCES * [framework-http-caching-headers.md](framework-http-caching-headers.md): Cache-Control, ETag, Last-Modified, Expires, Vary, Age. Vary: Origin is part of the CORS pattern and shares mechanics with Vary: Accept-Encoding from the caching framework. * [framework-http-content-headers.md](framework-http-content-headers.md): Content-Type, Content-Language, Content-Encoding, Content-Length, Content-Disposition. Content-Type values determine whether a request preflights (application/json preflights; form encoded does not). * [framework-http-seo-headers.md](framework-http-seo-headers.md): X-Robots-Tag, Link, Location. API endpoints should typically be noindex. * [framework-http-security-headers.md](framework-http-security-headers.md): HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CORP, COEP, COOP. CORS is closely related to but distinct from CORP. CORS is what the origin says about who may read its responses; CORP is what the resource owner says about who may load it at all. * [framework-http-performance-headers.md](framework-http-performance-headers.md): Server-Timing, Timing-Allow-Origin, Server, Alt-Svc. Timing-Allow-Origin is the CORS equivalent for the Resource Timing API. Server-Timing requires Access-Control-Expose-Headers to be readable from cross origin JavaScript. * [UNIVERSAL-RANKING-FRAMEWORK.md](UNIVERSAL-RANKING-FRAMEWORK.md): the master ranking reference. * [SEO-BUILD-REFERENCE.md](SEO-BUILD-REFERENCE.md) v2.4: the build playbook. * [framework-fastapi-sidecar.md](framework-fastapi-sidecar.md): FastAPI patterns including CORSMiddleware usage. * WHATWG Fetch (CORS spec): https://fetch.spec.whatwg.org/ * MDN CORS guide: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS * MDN Access-Control-Allow-Origin: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Origin * MDN Access-Control-Allow-Credentials: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Credentials * MDN Access-Control-Max-Age: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Max-Age * enable-cors.org: https://enable-cors.org/ * nginx `if` is evil article: https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/ --- ## APPENDIX A: ONE PAGE QUICK REFERENCE For the person who just wants the answer. ### Bubbles default CORS pattern for authenticated API ```nginx # http context map $http_origin $cors_origin { default ""; "~^https://app\.example\.com$" "$http_origin"; } # server / location context location /api/ { if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-CSRF-Token" always; add_header Access-Control-Allow-Credentials "true" always; add_header Access-Control-Max-Age "86400" always; add_header Vary "Origin" always; return 204; } if ($cors_origin) { add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Credentials "true" always; add_header Access-Control-Expose-Headers "X-Total-Count, Server-Timing, ETag, Link" always; add_header Vary "Origin" always; } proxy_pass http://127.0.0.1:9090; } ``` ### Public read only API ```nginx location /api/public/ { if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin "*" always; add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always; add_header Access-Control-Max-Age "86400" always; return 204; } add_header Access-Control-Allow-Origin "*" always; proxy_pass http://127.0.0.1:9090; } ``` ### Header decision matrix | Header | Preflight | Actual response | With credentials? | |---|---|---|---| | Access-Control-Allow-Origin | Yes | Yes | Specific value (no `*`) | | Access-Control-Allow-Methods | Yes | n/a | Specific value (no `*`) | | Access-Control-Allow-Headers | Yes | n/a | Specific value (no `*`) | | Access-Control-Allow-Credentials | Yes | Yes | `true` | | Access-Control-Max-Age | Yes | n/a | Specific seconds value | | Access-Control-Expose-Headers | n/a | Yes | Specific value (no `*`) | | Vary: Origin | Yes | Yes | Required when Allow-Origin is dynamic | ### Five rules to memorize 1. Never combine `*` with credentials. Browser refuses. 2. Always validate origin against an allow list. Never reflect blindly. 3. Always set `Vary: Origin` when Allow-Origin is dynamic. 4. Always set `Max-Age` (use 86400) to avoid every API call preflighting. 5. Custom request headers go in Allow-Headers; custom response headers go in Expose-Headers. ### Five commands every operator should know ```bash # 1. Simulate preflight curl -sI -X OPTIONS -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: POST" \ https://api.example.com/data # 2. Simulate actual request curl -sI -H "Origin: https://app.example.com" \ https://api.example.com/data | grep -i access-control # 3. Test from disallowed origin (should not get Allow-Origin) curl -sI -H "Origin: https://evil.com" \ https://api.example.com/data | grep -i access-control-allow-origin # 4. Find any unvalidated origin reflection (security audit) nginx -T 2>/dev/null | grep "Access-Control-Allow-Origin.*\$http_origin" # Any output is potentially a vulnerability # 5. Apply changes nginx -t && systemctl reload nginx ``` ### Three end to end tests ```bash # 1. Preflight succeeds curl -sI -X OPTIONS -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: PUT" \ https://api.example.com/data | head -1 # Expected: HTTP/2 204 # 2. Disallowed origin gets no Allow-Origin curl -sI -H "Origin: https://evil.com" \ https://api.example.com/data | grep -i access-control-allow-origin | wc -l # Expected: 0 # 3. Vary: Origin is set on dynamic responses curl -sI -H "Origin: https://app.example.com" \ https://api.example.com/data | grep -i vary | grep -i origin # Expected: vary: Origin ``` If all three produce expected output AND browser DevTools shows no CORS errors on production traffic, the CORS stack is correctly wired. --- End of framework-http-cors-headers.md.

Search equations, builds, manuscript text, and recovered sources.