CORS
ActiveCross-Origin Resource Sharing (CORS) is a W3C mechanism that allows web pages to make requests to a different origin than the one that served the page. Browsers enforce the Same-Origin Policy (SOP) which blocks cross-origin fetch requests by default. CORS allows servers to opt into cross-origin access by including specific HTTP response headers. CORS is defined in the W3C Fetch Living Standard §3.2 and is the most frequently encountered HTTP security mechanism in web development.
In one line
CORS (W3C Fetch spec §3.2) controls which cross-origin requests browsers allow. Browsers enforce the Same-Origin Policy – a script at https://app.com cannot call https://api.com unless the API server returns CORS headers. Simple requests (GET/POST with safe headers) are sent directly; the response must include Access-Control-Allow-Origin. Complex requests (custom headers, non-safe methods) first trigger an OPTIONS preflight that checks permissions before the actual request. Misconfigured CORS (wildcard + credentials, reflect-origin) is a critical security vulnerability.
Quick Reference
| Field | Size | Description |
|---|---|---|
| Same-Origin Policy | Browser enforcement | Browsers block cross-origin HTTP requests from JavaScript by default. Two URLs have the same origin only if scheme + host + port are all identical. https://a.com and http://a.com differ (scheme). api.a.com and a.com differ (host). |
| Simple requests | GET / POST / HEAD | Sent directly without preflight. Method: GET, HEAD, or POST. Headers: Accept, Accept-Language, Content-Language, Content-Type (only: text/plain, multipart/form-data, application/x-www-form-urlencoded). Response must include Access-Control-Allow-Origin. |
| Preflight | OPTIONS request first | Sent before requests that are not 'simple' – custom headers, PUT/PATCH/DELETE, or Content-Type: application/json. Browser sends OPTIONS with Access-Control-Request-Method and Access-Control-Request-Headers. Server must respond with matching Access-Control-Allow-* headers. |
| Access-Control-Allow-Origin | * or specific origin | * allows any origin (no credentials). Specific origin: 'https://app.com' allows only that origin. Reflect-Origin pattern (echo the Origin header) is a misconfiguration – validate against an allowlist first. |
| Credentials | withCredentials flag | Cookies and Authorization headers are not sent cross-origin by default. To include credentials: request must set credentials: 'include'; server MUST respond with Access-Control-Allow-Credentials: true AND a specific origin (not *). Never use * with credentials. |
| Preflight cache | Access-Control-Max-Age | Access-Control-Max-Age: 86400 caches the preflight result for 86400 seconds (24 hours). Avoids OPTIONS roundtrip on repeat requests. Maximum is typically 7200 (Chrome) or 86400 (Firefox) seconds. |
| Exposed headers | Access-Control-Expose-Headers | By default, JS can only read 7 'safe' response headers. To expose custom headers (X-Request-ID, RateLimit-Remaining): Access-Control-Expose-Headers: X-Request-ID, RateLimit-Remaining. |
| Origin header | Sent by browser | Browser adds Origin: https://app.com to cross-origin requests. Origin is null for file:// and some redirect cases. Servers SHOULD NOT use Referer for CORS validation – use Origin. |
Key Characteristics
Browser-enforced, not server-enforced
CORS headers tell the browser whether to allow the JS code to read the response. The server still processes the request – CORS does not prevent the request from reaching the server. To protect the server, use authentication.
Critical misconfiguration: wildcard + credentials
Access-Control-Allow-Origin: * combined with Access-Control-Allow-Credentials: true is invalid per spec and rejected by browsers. If you need credentials, you must specify the exact origin. Never use * for credentialed endpoints.
Reflect-Origin is a vulnerability
Blindly echoing the Origin header (Access-Control-Allow-Origin: req.headers.origin) without allowlist validation allows any origin to access your API. Always validate against a whitelist of known-good origins.
Preflight adds latency
Every non-simple request triggers an OPTIONS preflight – an extra round trip before the actual request. Minimize preflight by: caching with Access-Control-Max-Age, making requests 'simple' where possible, or using a same-origin proxy.
Message Format
# Simple GET request (no preflight)
GET /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Accept: application/json
# Server response (CORS allowed)
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Request-ID
Vary: Origin
Content-Type: application/json
# Preflight for POST with JSON body
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization# Preflight response (server grants permission)
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Request-ID
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Vary: Origin
# Then the actual POST is sent and the same CORS headers must be on the response
# CORS error example (blocked by browser)
# Server returns: Access-Control-Allow-Origin: https://other.com
# Browser sees: origin https://app.example.com does not match
# Browser blocks JS from reading response
# Console: "CORS policy: No 'Access-Control-Allow-Origin' header present"
# Vary: Origin – critical for CDN caching
# Without Vary: Origin, a CDN may cache the CORS response for one origin
# and return it to requests from a different originImplementations
Edge cases
- •null Origin: file:// URLs and some redirect flows send Origin: null. Never allowlist null – it allows any local file or sandboxed iframe to make credentialed requests.
- •Vary: Origin is required on CORS responses to prevent CDNs from caching the wrong origin-specific response. Missing Vary: Origin on a CDN-cached API causes intermittent CORS failures.
- •OPTIONS preflight to 401: many API gateways return 401 Unauthorized on OPTIONS preflight before CORS headers are added. The browser sees 401 with no CORS headers and reports a CORS error – the fix is to allow OPTIONS through before authentication.
- •Credentialed requests require specific origin, not * – using * with credentials is a spec violation and browsers reject it.