Cross-Origin Resource Sharing (CORS) is one of the most misunderstood and misconfigured security mechanisms in web applications. According to recent security research, nearly 90% of API breaches involve misconfigured CORS policies. This guide explains how CORS works, common vulnerabilities, and how to implement secure configurations.
What is CORS and Why Does It Matter?
CORS is a browser security mechanism that controls how web pages from one origin (domain) can request resources from another origin. Without CORS, the browser's same-origin policy would block all cross-origin requests, making modern web applications impossible.
Here's what happens during a cross-origin request:
- Browser sends request with
Originheader - Server responds with
Access-Control-Allow-Originheader - Browser compares origins and allows or blocks the response
Request:
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Key CORS Headers Explained
| Header | Purpose | Example |
|---|---|---|
Access-Control-Allow-Origin | Specifies allowed origins | https://example.com or * |
Access-Control-Allow-Credentials | Allows cookies/auth headers | true |
Access-Control-Allow-Methods | Allowed HTTP methods | GET, POST, PUT |
Access-Control-Allow-Headers | Allowed request headers | Content-Type, Authorization |
Access-Control-Max-Age | Preflight cache duration (seconds) | 86400 |
Vary | Cache key for responses | Origin |
Understanding Preflight Requests
Browsers send preflight OPTIONS requests before "complex" cross-origin requests. A request is considered complex if it:
- Uses methods other than GET, HEAD, or POST
- Includes custom headers
- Uses Content-Type other than
application/x-www-form-urlencoded,multipart/form-data, ortext/plain
Preflight Request:
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
Preflight Response:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Common CORS Misconfigurations
1. Wildcard Origin with Credentials (Critical)
The vulnerability:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
This configuration is impossible according to the CORS specification - browsers reject it. However, many developers work around this by reflecting any origin, which is even worse.
Why it's dangerous: Any website can make authenticated requests to your API using the victim's cookies.
Attack scenario:
- User is logged into your application
- User visits attacker's website
- Attacker's JavaScript makes requests to your API
- Browser includes user's cookies automatically
- Attacker receives sensitive data from your API
2. Origin Reflection (Critical)
The vulnerability:
# Dangerous: Reflects any origin
allowed_origin = request.headers.get('Origin')
response.headers['Access-Control-Allow-Origin'] = allowed_origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
Why it's dangerous: The server accepts ANY origin, completely bypassing CORS protection.
Secure alternative:
ALLOWED_ORIGINS = {'https://app.example.com', 'https://admin.example.com'}
origin = request.headers.get('Origin')
if origin in ALLOWED_ORIGINS:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
response.headers['Vary'] = 'Origin'
3. Null Origin Acceptance (High)
The vulnerability:
Access-Control-Allow-Origin: null
Why it's dangerous: The null origin can be triggered from:
- Sandboxed iframes (
<iframe sandbox="allow-scripts">) - Local file:// URLs
- Data URLs
- Redirects from HTTP to HTTPS
Attackers can craft pages that send requests with a null origin.
4. Trusting All Subdomains (Medium)
The vulnerability:
origin = request.headers.get('Origin')
if origin and origin.endswith('.example.com'):
# Accepts evil.example.com, anything.example.com, etc.
response.headers['Access-Control-Allow-Origin'] = origin
Why it's dangerous: If any subdomain is compromised (XSS, subdomain takeover), attackers can access your API.
Secure alternative:
ALLOWED_SUBDOMAINS = {'app.example.com', 'api.example.com'}
parsed = urlparse(origin)
if parsed.netloc in ALLOWED_SUBDOMAINS:
response.headers['Access-Control-Allow-Origin'] = origin
5. Missing Vary Header (Medium)
The vulnerability: Not including Vary: Origin when the CORS response varies by origin.
Why it's dangerous: CDNs and browsers may cache responses with the wrong CORS headers, causing legitimate requests to fail or allowing unauthorized access.
Fix: Always include Vary: Origin when your CORS response depends on the request origin:
Vary: Origin
Implementing Secure CORS
Step 1: Define Your Origin Whitelist
// Express.js example
const allowedOrigins = new Set([
'https://app.example.com',
'https://admin.example.com',
'https://mobile.example.com'
]);
Step 2: Validate Origins Strictly
const cors = require('cors');
const corsOptions = {
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, curl)
if (!origin) return callback(null, true);
if (allowedOrigins.has(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400 // 24 hours
};
app.use(cors(corsOptions));
Step 3: Restrict Methods and Headers
Only allow methods and headers your API actually needs:
methods: ['GET', 'POST'], // Not PUT, DELETE unless needed
allowedHeaders: ['Content-Type', 'Authorization']
Step 4: Add Vary Header
Ensure proper caching behavior:
app.use((req, res, next) => {
res.header('Vary', 'Origin');
next();
});
Step 5: Set Reasonable Max-Age
Balance security with performance:
maxAge: 86400 // 24 hours - re-validate daily
Testing Your CORS Configuration
Using curl
# Test basic CORS
curl -I -X OPTIONS https://api.example.com/endpoint \
-H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: GET"
# Test with credentials
curl -I https://api.example.com/endpoint \
-H "Origin: https://evil.com" \
-H "Cookie: session=abc123"
What to Check
- Wildcard with credentials: Does the API return
*withcredentials: true? - Origin reflection: Does the API echo back any origin you send?
- Null origin: Does the API accept
Origin: null? - Subdomain bypass: Does
evil.example.comget accepted? - Method exposure: Are dangerous methods (DELETE, PATCH) allowed?
- Vary header: Is
Vary: Originpresent?
Automated Testing
Include CORS tests in your CI/CD pipeline:
describe('CORS Security', () => {
it('should reject unauthorized origins', async () => {
const response = await fetch('https://api.example.com/data', {
headers: { 'Origin': 'https://evil.com' }
});
expect(response.headers.get('Access-Control-Allow-Origin'))
.not.toBe('https://evil.com');
});
it('should not reflect arbitrary origins', async () => {
const response = await fetch('https://api.example.com/data', {
headers: { 'Origin': 'https://random-attacker.com' }
});
expect(response.headers.get('Access-Control-Allow-Origin'))
.toBeNull();
});
});
Framework-Specific Configurations
Express.js
const cors = require('cors');
app.use(cors({
origin: ['https://app.example.com'],
credentials: true,
methods: ['GET', 'POST'],
maxAge: 86400
}));
Django
# settings.py
CORS_ALLOWED_ORIGINS = [
"https://app.example.com",
]
CORS_ALLOW_CREDENTIALS = True
CORS_PREFLIGHT_MAX_AGE = 86400
Flask
from flask_cors import CORS
CORS(app,
origins=['https://app.example.com'],
supports_credentials=True,
max_age=86400)
Spring Boot
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com")
.allowCredentials(true)
.allowedMethods("GET", "POST")
.maxAge(86400);
}
}
CORS Security Checklist
- Origin whitelist defined (no wildcards in production)
- No origin reflection (server-side validation, not echo)
- Null origin rejected
- Subdomain validation uses exact matching
- Credentials only for specific trusted origins
- HTTP methods restricted to minimum needed
- Custom headers restricted to minimum needed
- Vary: Origin header present
- Max-Age set to reasonable value (≤24 hours)
- CORS configuration tested in CI/CD
- Regular security audits include CORS review
Conclusion
CORS misconfigurations are among the most common and dangerous web security vulnerabilities. By implementing strict origin whitelists, avoiding origin reflection, and testing your configuration regularly, you can protect your APIs from cross-origin attacks.
Use our CORS Policy Analyzer to test your current configuration and identify vulnerabilities before attackers do.
Related Resources
- Security Headers Analyzer - Check all your HTTP security headers
- CSP Generator - Create Content Security Policies
- OWASP CORS Guide - OWASP reference