CORS Security
Configure CORS headers correctly to control cross-origin resource access while preventing overly permissive policies.
You are an expert in configuring Cross-Origin Resource Sharing (CORS) policies to enable legitimate cross-origin access while preventing unauthorized access to APIs and resources. ## Key Points - `https://app.example.com` and `https://app.example.com/api` — same origin - `https://app.example.com` and `http://app.example.com` — different (scheme) - `https://app.example.com` and `https://api.example.com` — different (host) - `https://app.example.com` and `https://app.example.com:8443` — different (port) - **Simple requests**: GET, HEAD, POST with standard content types. The browser sends the request directly and checks CORS headers on the response. 1. **Never use `Access-Control-Allow-Origin: *` with credentials**: Browsers reject this combination, but reflecting any origin with credentials is equally dangerous. 2. **Maintain an explicit allowlist of origins**: Avoid reflecting the request Origin header without validation. 3. **Restrict methods and headers**: Only allow the HTTP methods and headers your API actually uses. 4. **Set `Max-Age` for preflight caching**: Reduces the number of OPTIONS requests. 86400 (24 hours) is a common value. 5. **Use per-route CORS where needed**: Public endpoints can be more permissive; authenticated endpoints should be strict. 6. **Do not rely on CORS for security**: CORS is a browser mechanism. Server-to-server requests, curl, and non-browser clients bypass it entirely. Always authenticate and authorize on the server. 7. **Handle the null origin carefully**: Sandboxed iframes and local file origins send `Origin: null`. Do not allowlist `null` as a string.
skilldb get security-practices-skills/CORS SecurityFull skill: 230 linesCORS Configuration — Application Security
You are an expert in configuring Cross-Origin Resource Sharing (CORS) policies to enable legitimate cross-origin access while preventing unauthorized access to APIs and resources.
Core Philosophy
CORS is not a security mechanism you add to protect your application; it is a controlled relaxation of the browser's default Same-Origin Policy. The mindset should always be "deny by default, allow by exception." Every origin you add to the allowlist is an expansion of your attack surface, and each one should be justified by a concrete business requirement.
The most important insight about CORS is that it only governs browser behavior. Server-to-server requests, command-line tools, and mobile apps bypass CORS entirely. This means CORS can never replace authentication, authorization, or input validation. Its role is narrowly scoped: preventing a malicious website from using a victim's browser as a proxy to make authenticated requests to your API. When developers mistake CORS for a general-purpose access control mechanism, they either over-rely on it (leaving server-side checks weak) or over-configure it (adding complexity without security benefit).
A well-managed CORS configuration reflects the application's actual deployment topology. It knows exactly which frontend origins need access, which HTTP methods each endpoint supports, and whether credentials are required. It evolves with the architecture — when a frontend is decommissioned, its origin is removed; when a new service is added, its origin is explicitly approved. Treating CORS configuration as living infrastructure rather than a set-and-forget header prevents the gradual drift toward overly permissive policies.
Anti-Patterns
-
Reflecting the request Origin header without validation: Dynamically mirroring whatever origin the browser sends in the
Access-Control-Allow-Originresponse header effectively makes CORS open to every site on the internet, defeating its entire purpose. -
Using
Access-Control-Allow-Origin: *on authenticated endpoints: While the browser blocks this combination with credentials, developers sometimes work around it by reflecting the origin, which is equally dangerous and allows any site to make credentialed requests. -
Configuring CORS once and never revisiting it: As frontends are added, removed, or migrated to new domains, stale origins accumulate in the allowlist. Unused allowed origins are dormant vulnerabilities waiting to be exploited if those domains are ever taken over.
-
Applying the same permissive CORS policy to every route: Public health-check endpoints and authenticated mutation endpoints have very different security requirements. Applying a single broad policy to the entire API unnecessarily widens the attack surface for sensitive routes.
-
Assuming CORS errors mean "add more permissions": When a developer encounters a CORS error, the correct response is to understand why the request is cross-origin and whether it should be. The incorrect response is to keep widening the policy until the error disappears.
Overview
CORS is a browser security mechanism that controls which origins can make requests to your server. By default, browsers block cross-origin requests (the Same-Origin Policy). CORS headers tell the browser which exceptions to allow. Misconfigured CORS can expose APIs to unauthorized origins, leak credentials, or enable data theft.
Core Concepts
Same-Origin Policy
Two URLs share the same origin if and only if they have the same scheme, host, and port:
https://app.example.comandhttps://app.example.com/api— same originhttps://app.example.comandhttp://app.example.com— different (scheme)https://app.example.comandhttps://api.example.com— different (host)https://app.example.comandhttps://app.example.com:8443— different (port)
CORS Headers
| Header | Purpose |
|---|---|
Access-Control-Allow-Origin | Which origins are allowed (* or a specific origin) |
Access-Control-Allow-Methods | Allowed HTTP methods for preflight |
Access-Control-Allow-Headers | Allowed request headers for preflight |
Access-Control-Allow-Credentials | Whether cookies/auth headers are sent (true or omit) |
Access-Control-Expose-Headers | Which response headers the client can read |
Access-Control-Max-Age | How long the preflight result is cached (seconds) |
Simple vs. Preflighted Requests
- Simple requests: GET, HEAD, POST with standard content types. The browser sends the request directly and checks CORS headers on the response.
- Preflighted requests: Requests with custom headers, non-standard methods (PUT, DELETE), or non-standard content types trigger an OPTIONS preflight. The browser asks permission before sending the actual request.
The Credential Rule
When Access-Control-Allow-Credentials: true is set, Access-Control-Allow-Origin must not be *. It must be a specific origin. This prevents credential leakage to arbitrary origins.
Implementation Patterns
Express with cors Middleware
const cors = require('cors');
// GOOD — explicit allowlist
const ALLOWED_ORIGINS = [
'https://app.example.com',
'https://admin.example.com',
];
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (server-to-server, mobile apps)
if (!origin) return callback(null, true);
if (ALLOWED_ORIGINS.includes(origin)) {
return callback(null, true);
}
return callback(new Error('Not allowed by CORS'));
},
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
credentials: true,
maxAge: 86400, // cache preflight for 24 hours
}));
Environment-Aware CORS
function getCorsConfig() {
if (process.env.NODE_ENV === 'development') {
return {
origin: ['http://localhost:3000', 'http://localhost:5173'],
credentials: true,
};
}
return {
origin: ['https://app.example.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400,
};
}
app.use(cors(getCorsConfig()));
FastAPI (Python)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
ALLOWED_ORIGINS = [
"https://app.example.com",
"https://admin.example.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS, # not ["*"] with credentials
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
max_age=86400,
)
Nginx CORS Headers
server {
listen 443 ssl;
server_name api.example.com;
# Map allowed origins
set $cors_origin "";
if ($http_origin ~* "^https://(app|admin)\.example\.com$") {
set $cors_origin $http_origin;
}
location /api/ {
# Handle preflight
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
proxy_pass http://backend;
}
}
Per-Route CORS (Express)
const publicCors = cors({ origin: '*', methods: ['GET'] });
const authedCors = cors({
origin: ['https://app.example.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
});
// Public endpoints — open CORS, no credentials
app.get('/api/public/health', publicCors, healthHandler);
app.get('/api/public/docs', publicCors, docsHandler);
// Authenticated endpoints — restricted origin
app.use('/api/v1', authedCors, authMiddleware, apiRouter);
Dynamic Origin with Subdomain Pattern
app.use(cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
// Allow any subdomain of example.com over HTTPS
const pattern = /^https:\/\/([a-z0-9-]+\.)?example\.com$/;
if (pattern.test(origin)) {
return callback(null, true);
}
callback(new Error('Origin not allowed'));
},
credentials: true,
}));
Best Practices
- Never use
Access-Control-Allow-Origin: *with credentials: Browsers reject this combination, but reflecting any origin with credentials is equally dangerous. - Maintain an explicit allowlist of origins: Avoid reflecting the request Origin header without validation.
- Restrict methods and headers: Only allow the HTTP methods and headers your API actually uses.
- Set
Max-Agefor preflight caching: Reduces the number of OPTIONS requests. 86400 (24 hours) is a common value. - Use per-route CORS where needed: Public endpoints can be more permissive; authenticated endpoints should be strict.
- Do not rely on CORS for security: CORS is a browser mechanism. Server-to-server requests, curl, and non-browser clients bypass it entirely. Always authenticate and authorize on the server.
- Handle the null origin carefully: Sandboxed iframes and local file origins send
Origin: null. Do not allowlistnullas a string. - Test CORS configuration: Use
curl -H "Origin: https://evil.com" -Ito verify headers are not reflected for unauthorized origins.
Common Pitfalls
- Reflecting the Origin header without validation:
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)allows every origin. Always check against an allowlist. - Using
*withcredentials: true: This is invalid per the spec. Browsers will block the response. - Forgetting preflight for custom headers: If your frontend sends
AuthorizationorX-Custom-Header, the browser issues a preflight. The server must respond to OPTIONS with the correct headers. - Allowing overly broad subdomain patterns:
*.example.commay include user-controlled subdomains if you offer custom subdomains. - Not setting CORS headers on error responses: If your error handler does not include CORS headers, the browser cannot read the error message. Use
alwaysin Nginx or handle errors in the CORS middleware. - Assuming CORS replaces authentication: CORS does not authenticate or authorize. It is only a browser-level access control on which origins can read responses.
Install this skill directly: skilldb add security-practices-skills
Related Skills
Content Security Policy
Configure Content-Security-Policy headers to mitigate XSS, data injection, and clickjacking attacks.
CSRF Protection
Protect web applications against cross-site request forgery (CSRF) using tokens, SameSite cookies, and origin validation.
Input Validation
Validate and sanitize all user input at application boundaries using schemas, type coercion, and allowlists.
Secrets Management
Securely store, access, rotate, and audit application secrets and credentials using vaults, environment variables, and CI/CD integrations.
SQL Injection
Prevent SQL injection attacks using parameterized queries, ORM best practices, and input validation layers.
Supply Chain Security
Secure your software supply chain by auditing dependencies, pinning versions, verifying integrity, and monitoring for vulnerabilities.