CORS
Cross-Origin Resource Sharing explained
CORS (Cross-Origin Resource Sharing) is a security mechanism that allows web pages to request resources from a different domain than the one serving the page. Without CORS, browsers block cross-origin requests by default for security reasons. CORS uses HTTP headers to tell browsers to permit cross-origin access.
The Same-Origin Policy
By default, browsers enforce the Same-Origin Policy:
https://example.com/page1 can request:
✅ https://example.com/page2 (same origin)
❌ https://api.example.com/data (different subdomain)
❌ http://example.com/page (different protocol)
❌ https://example.com:8080/page (different port)
❌ https://other-site.com/page (different domain)
Two URLs have the same origin only if they share:
- Protocol (http vs https)
- Domain (including subdomains)
- Port
Simple CORS Requests
Some requests don’t trigger CORS preflight:
- GET, HEAD, POST methods
- Only specific headers (Accept, Accept-Language, Content-Language, Content-Type)
- Content-Type only: application/x-www-form-urlencoded, multipart/form-data, text/plain
// Simple GET request
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data));
Server response headers:
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST
Preflight Requests
For non-simple requests, browsers send a preflight OPTIONS request:
// This triggers preflight
fetch('https://api.example.com/data', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
}
});
Request flow:
OPTIONS /data HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: X-Custom-Header
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 86400
Then the actual request proceeds.
CORS Headers
Response Headers (Server → Browser)
| Header | Description |
|---|---|
Access-Control-Allow-Origin | Which origins can access (or *) |
Access-Control-Allow-Methods | Allowed HTTP methods |
Access-Control-Allow-Headers | Allowed custom headers |
Access-Control-Allow-Credentials | Whether cookies/auth are allowed |
Access-Control-Expose-Headers | Which response headers JS can access |
Access-Control-Max-Age | How long to cache preflight results |
Request Headers (Browser → Server)
| Header | Description |
|---|---|
Origin | The requesting origin (always sent) |
Access-Control-Request-Method | Method for preflight |
Access-Control-Request-Headers | Headers for preflight |
Common Errors
1. No Access-Control-Allow-Origin
Access to fetch at 'https://api.example.com/data' from origin
'https://example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present.
Fix: Server must include Access-Control-Allow-Origin header.
2. Credentials Without Specific Origin
The value of the 'Access-Control-Allow-Origin' header in the
response must not be the wildcard '*' when the request's
credentials mode is 'include'.
Fix: Use specific origin, not *, when sending credentials:
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
3. Preflight Failed
Request header field X-Custom-Header is not allowed by
Access-Control-Allow-Headers in preflight response.
Fix: Server must whitelist custom headers.
Implementing CORS (Server-Side)
Express.js
// Simple setup
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});
// Or use cors middleware
const cors = require('cors');
// Allow all origins
app.use(cors());
// Restrict to specific origins
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
credentials: true,
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
Nginx
location /api {
add_header 'Access-Control-Allow-Origin' 'https://example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
# Handle preflight
if ($request_method = 'OPTIONS') {
return 204;
}
}
Credentials and Cookies
To include cookies/auth tokens:
fetch('https://api.example.com/data', {
credentials: 'include', // Include cookies
headers: {
'Authorization': 'Bearer token123'
}
});
Server must respond with:
Access-Control-Allow-Origin: https://example.com # Cannot be *
Access-Control-Allow-Credentials: true
Security Considerations
Never Use Wildcard with Credentials
# DANGEROUS - Don't do this!
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Validate Origins
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
next();
});
Consider SameSite Cookies
As an alternative to CORS, consider using SameSite cookies:
Set-Cookie: session=abc123; SameSite=None; Secure
CORS is a browser security feature, not a server security feature. Configure it properly: always validate origins instead of using wildcard with credentials, whitelist only necessary headers and methods, and understand that preflight requests add latency. For public APIs, Access-Control-Allow-Origin: * is fine. For authenticated endpoints, always specify exact origins.
Debugging CORS
- Check Network Tab: Look for preflight OPTIONS requests
- Response Headers: Verify
Access-Control-Allow-*headers are present - Credentials Mode: Ensure client and server agree on credentials
- Server Logs: Check if preflight requests reach your server
Remember: CORS errors are browser-enforced. The server might respond correctly, but the browser blocks access if headers are missing or incorrect.