CORS Deep Dive
Cross-Origin Resource Sharing (CORS) is the browser mechanism that controls when JavaScript on one origin can access resources on another. It's confusing because the protections are mostly browser-imposed (servers usually allow everything; browsers block).
This page is about how CORS actually works and the configuration patterns.
The same-origin policy
By default, browsers prevent JavaScript on `https://app.example.com` from making requests to `https://api.example.com`. The "origin" is scheme + host + port.
The protection is for users, not servers. The point: a malicious page can't read your bank's data even if you're logged in.
Why CORS exists
Without CORS, every cross-origin XHR/fetch would be blocked. Modern web apps make many cross-origin requests legitimately (calling APIs, embedding fonts, loading images). CORS is the mechanism for the server to opt into allowing specific cross-origin requests.
How CORS works
Simple requests
GET, HEAD, or POST with simple content types (form-data, text/plain) and no special headers can be sent immediately. The browser includes:
```
Origin: https://app.example.com
```
The server responds with:
```
Access-Control-Allow-Origin: https://app.example.com
```
(Or `*` for any origin, with limitations.)
If the response has the right header, the browser lets JS read it. If not, the browser blocks JS access (the request was sent and the response received, but JS can't read it).
Preflight requests
For requests that aren't simple (DELETE, PUT, custom headers, JSON content type), the browser sends an OPTIONS preflight request first:
```http
OPTIONS /api/orders
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization
```
Server responds:
```http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 3600
```
The browser then sends the actual request.
The preflight is cached per the Max-Age; subsequent requests skip it.
The major headers
`Access-Control-Allow-Origin`
The origin that's allowed. Either a specific origin or `*` (any).
`*` doesn't work with credentials (cookies, auth headers). For authenticated cross-origin requests, you must specify the origin.
`Access-Control-Allow-Credentials`
If `true`, cookies and auth headers can be sent. Requires specific Allow-Origin (not `*`).
`Access-Control-Allow-Methods`
Methods the resource supports. For preflight responses.
`Access-Control-Allow-Headers`
Headers the request can include. For preflight responses.
`Access-Control-Expose-Headers`
Headers JS can read. By default, only a subset of response headers are visible to JS; this header expands the set.
`Access-Control-Max-Age`
How long preflight result is cached. Longer means fewer preflights.
Patterns that work
API gateway with explicit allowed origins
```
Allow-Origin: comes from a list — production-app.example.com, staging.example.com, localhost:3000 for dev
```
Each request's Origin header is checked against the list; matching origin is echoed back. Not in list: no header, browser blocks.
This is the right pattern for production APIs.
Wildcard for public resources
```
Access-Control-Allow-Origin: *
```
For genuinely public APIs (read-only, no credentials needed), wildcard is fine.
Subdomain handling
If you control multiple subdomains:
```
*.example.com → all subdomains allowed
```
Implemented by checking the Origin header; not by literal wildcard in the header value (browsers don't support wildcards in Allow-Origin beyond `*`).
Common configuration errors
Returning multiple Allow-Origin headers
Browsers reject. Specify one origin (echoed from request) or `*`.
Wildcard with credentials
```
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
```
Browser rejects. Use specific origin.
Forgetting OPTIONS
The preflight needs a 200/204 response. If the API returns 401 for OPTIONS (because not authenticated), CORS fails.
Configure the framework to short-circuit OPTIONS to a CORS-aware handler before auth.
Cache headers ignored
Different origins might have different CORS rules. If responses are cached, the browser might return the wrong CORS headers. Use `Vary: Origin` to inform caches.
CORS isn't security
A common misunderstanding: CORS protects the server. It doesn't.
CORS protects users from malicious scripts in their browser reading data they shouldn't. The server still receives every request; the browser is what blocks JS access to responses.
Server-side authentication and authorization are still required. CORS is in addition to, not instead of.
CORS in different scenarios
Browser → public API
Standard CORS. Origin specific or wildcard.
Browser → authenticated API
Specific origin; allow-credentials true; cookies or auth headers.
Server → server
CORS doesn't apply. Servers don't enforce same-origin policy. CORS is browser-only.
Mobile apps → API
Mobile apps don't have a browser; CORS doesn't apply. The app's HTTP client just makes the request.
CLI tools → API
Same — no CORS.
Common failure patterns
- **Setting `Access-Control-Allow-Origin: *` with credentials.** Doesn't work; pick specific origin.
- **OPTIONS not handled.** Preflight fails; actual request never happens.
- **Caching CORS responses without Vary.** Wrong headers cached.
- **Treating CORS as security.** It's not; server-side auth still needed.
- **Adding CORS in code path-by-path.** Centralize at middleware/gateway level.
- **Different CORS for different endpoints.** Maintenance nightmare; standardize.
Further Reading
- [HttpTwoAndHttpThree](HttpTwoAndHttpThree) — Underlying HTTP
- [Web Services and APIs Hub](WebServicesAndApisHub) — APIs that use CORS
- [WebApplicationFirewalls](WebApplicationFirewalls) — Edge layer
- [Networking Hub](NetworkingHub) — Cluster index