AI Opens CORS to the Entire Internet
AI generates CORS with: Access-Control-Allow-Origin: * (every origin allowed), credentials enabled with wildcard origin (browsers reject this, but AI does not know), no preflight optimization (every complex request triggers a preflight round trip), no exposed headers (custom headers invisible to JavaScript), and identical config for dev and production (localhost origins in production). Every one of these is either a security hole or a performance problem.
Modern CORS is: origin-specific (allowlist of trusted origins, not wildcard), credential-aware (specific origin required when cookies are sent), preflight-cached (Access-Control-Max-Age reduces preflight frequency), header-explicit (expose only the headers clients need), and environment-differentiated (localhost in dev, production domains in prod). AI generates none of these.
These rules cover: origin allowlists, credential handling, preflight optimization, exposed headers, and per-environment CORS policies.
Rule 1: Origin Allowlists, Not Wildcards
The rule: 'Define an explicit list of allowed origins: const ALLOWED_ORIGINS = ["https://rulesync.com", "https://app.rulesync.com", "https://docs.rulesync.com"]. On each request, check the Origin header against the list. If it matches, set Access-Control-Allow-Origin to that specific origin. If not, omit the header (the browser blocks the request). Never use * in production when cookies or authentication are involved.'
For dynamic origin validation: 'Implement origin checking in middleware: const origin = req.headers.origin; if (ALLOWED_ORIGINS.includes(origin)) { res.setHeader("Access-Control-Allow-Origin", origin); res.setHeader("Vary", "Origin"); }. The Vary: Origin header is critical — it tells caches that the response varies by origin. Without it, a cached response for origin A may be served to origin B with the wrong CORS header.'
AI generates: app.use(cors({ origin: "*" })) — every website on the internet can make requests to your API. For public, read-only APIs (CDNs, public datasets) this is acceptable. For authenticated APIs with cookies or tokens, wildcard origin means: any malicious site can make authenticated requests on behalf of your users. Origin allowlists restrict this to your trusted domains.
- Explicit origin allowlist: ["https://rulesync.com", "https://app.rulesync.com"]
- Check Origin header against list — set ACAO to matching origin, not wildcard
- Vary: Origin header — critical for CDN/proxy caching correctness
- Wildcard (*) only for public read-only APIs — never with credentials
- Reject unknown origins by omitting ACAO header — browser enforces the block
Without Vary: Origin, a CDN caches the response with ACAO set to origin A, then serves it to origin B with the wrong CORS header. Origin B gets blocked. One header — Vary: Origin — tells caches that the response differs by requesting origin.
Rule 2: Credentials Require Specific Origin
The rule: 'When your API uses cookies, session tokens, or HTTP authentication, set Access-Control-Allow-Credentials: true AND set Access-Control-Allow-Origin to the specific requesting origin (not *). Browsers enforce this: if credentials are included and ACAO is *, the request is blocked. This is a security feature — credentials should only be sent to origins you explicitly trust.'
For cookie-based auth: 'Frontend fetch must include credentials: fetch("https://api.rulesync.com/me", { credentials: "include" }). The server responds with: Access-Control-Allow-Origin: https://app.rulesync.com, Access-Control-Allow-Credentials: true. Both headers must be present. The origin must match exactly (no wildcards, no regex patterns in the header value). This ensures cookies are only sent to and accepted from known origins.'
AI generates: cors({ origin: "*", credentials: true }) — this does not work. The browser silently blocks the response because the spec forbids credentials with wildcard origin. The developer sees a CORS error, adds more permissive headers, and eventually gives up. The fix is simple: replace * with the specific origin string.
Rule 3: Preflight Request Optimization
The rule: 'Set Access-Control-Max-Age to cache preflight responses: Access-Control-Max-Age: 86400 (24 hours). Preflight (OPTIONS) requests occur before every complex request (custom headers, non-simple methods, non-simple content types). Without caching, every API call triggers two HTTP requests: OPTIONS then the actual request. With Max-Age, the browser caches the preflight result and skips OPTIONS for subsequent requests to the same endpoint.'
For what triggers preflight: 'Simple requests (GET/POST with standard headers and form content type) do not trigger preflight. Complex requests do: custom headers (Authorization, X-API-Key), non-standard methods (PUT, DELETE, PATCH), JSON content type (application/json), and custom content types. Most API requests are complex — they use Authorization headers and JSON bodies. Preflight caching halves the request count for these.'
AI generates: no Access-Control-Max-Age header. Every single API call (GET with Authorization header = complex request) triggers an OPTIONS preflight first. For a dashboard that makes 20 API calls on load, that is 40 HTTP requests instead of 20. One header — Access-Control-Max-Age: 86400 — eliminates 20 preflight requests for 24 hours.
A dashboard making 20 API calls with Authorization headers triggers 20 preflight OPTIONS requests first — 40 total HTTP requests. Access-Control-Max-Age: 86400 caches the preflight for 24 hours. Same 20 calls, 20 requests. One header halves your traffic.
Rule 4: Explicit Exposed Headers
The rule: 'By default, JavaScript can only read these response headers: Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma. Custom headers (X-Request-Id, X-RateLimit-Remaining, Link, ETag) are invisible to JavaScript unless you expose them: Access-Control-Expose-Headers: X-Request-Id, X-RateLimit-Remaining, X-RateLimit-Limit, Link.'
For rate limiting headers: 'If your API returns rate limit information in headers (X-RateLimit-Remaining, X-RateLimit-Limit, X-RateLimit-Reset, Retry-After), clients need Access-Control-Expose-Headers to read them. Without it: the headers exist in the response (visible in DevTools) but are undefined when read via JavaScript. The client cannot show rate limit info or implement client-side throttling.'
AI generates: custom headers in responses with no exposed headers list. The frontend developer adds console.log(response.headers.get('X-Request-Id')) — null. They check DevTools — the header is there. They spend 30 minutes debugging what is actually a missing Expose-Headers configuration. One CORS header fixes it.
Rule 5: Per-Environment CORS Configuration
The rule: 'Maintain separate CORS configurations for each environment: development (localhost origins, permissive for DX), staging (staging domain origins, mirrors production restrictions), production (production domains only, strict). Never include localhost in production CORS. Never use wildcard in production. Load the allowed origins from environment configuration — not hardcoded in source.'
For development convenience: 'In development, allow http://localhost:3000, http://localhost:5173 (Vite), and other dev server origins. But make this conditional: const origins = process.env.NODE_ENV === "production" ? PROD_ORIGINS : [...PROD_ORIGINS, ...DEV_ORIGINS]. This gives developers freedom locally without opening production to localhost origins. Never ship localhost in production CORS — it means any developer machine can hit your production API.'
AI generates: cors({ origin: "http://localhost:3000" }) — hardcoded dev origin that breaks in production. Or cors({ origin: "*" }) — works everywhere but is insecure. Per-environment origin lists from env vars: works everywhere and is secure. Three lines of configuration, zero CORS surprises across environments.
cors({ origin: 'http://localhost:3000' }) in production means any developer machine can hit your production API. Per-environment origin lists: DEV_ORIGINS for development, PROD_ORIGINS for production. Three lines of config, zero environment-crossing.
Complete CORS Configuration Rules Template
Consolidated rules for CORS configuration.
- Origin allowlist: explicit trusted origins, never wildcard (*) with credentials
- Vary: Origin header: critical for correct CDN/proxy caching
- Credentials: true requires specific origin — browsers block credentials + wildcard
- Access-Control-Max-Age: 86400 — cache preflight for 24 hours, halve request count
- Expose-Headers for custom headers — X-Request-Id, rate limit headers, Link, ETag
- Per-environment origins: localhost in dev, production domains in prod
- Origins from env config — never hardcoded in source code
- Allowed methods: GET, POST, PUT, PATCH, DELETE — only what your API uses