CORS Security
CORS (Cross-Origin Resource Sharing) is a security mechanism implemented by modern browsers that controls how web applications can make HTTP requests to domains different from the one that served the original page, relaxing the Same-Origin Policy (SOP) in a controlled manner - a fundamental security policy that restricts scripts from one origin from accessing resources of another origin. Although CORS is essential for modern web application architectures, where front-ends frequently need to consume APIs hosted on different domains, its improper configuration represents one of the most common and dangerous vulnerabilities in contemporary web applications. CORS misconfigurations can expose sensitive data to unauthorized domains, enable Cross-Site Request Forgery (CSRF) attacks even in the presence of anti-CSRF tokens, facilitate credential theft, and in extreme cases, allow attackers to perform privileged actions on behalf of authenticated users. The problem is aggravated by the fact that many developers, upon encountering CORS errors during development, opt for overly permissive solutions (such as using the wildcard "*" or automatically reflecting the request origin) without fully understanding the security implications. This article explores in depth the fundamentals of CORS, common configuration vulnerabilities, and establishes robust practices for secure implementation across different platforms and frameworks, balancing functionality with an adequate defensive posture.
Same-Origin Policy (SOP)
Browsers implement SOP: scripts can only access resources from the same origin (protocol + domain + port). CORS relaxes SOP in a controlled manner.
# Same origin
https://example.com/api ← https://example.com/app [OK]
# Different origins (blocked by SOP)
https://example.com ← http://example.com (protocol)
https://example.com ← https://api.example.com (subdomain)
https://example.com ← https://example.com:8080 (port)
CORS Headers
Access-Control-Allow-Origin
# Allow specific origin (recommended)
Access-Control-Allow-Origin: https://trusted.com
# Allow any origin (DANGEROUS!)
Access-Control-Allow-Origin: *
# Dynamic based on whitelist (correct)
const allowedOrigins = ['https://app1.com', 'https://app2.com'];
const origin = request.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
Other Important Headers
# Allow credentials (cookies, auth headers)
Access-Control-Allow-Credentials: true
# Allowed HTTP methods
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
# Allowed headers in requests
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
# Headers exposed to client-side JavaScript
Access-Control-Expose-Headers: X-Custom-Header, X-Request-Id
# Preflight cache time (seconds)
Access-Control-Max-Age: 86400
Preflight Requests
Browsers send an OPTIONS request before "non-simple" requests to verify permissions.
# Client sends preflight
OPTIONS /api/resource HTTP/1.1
Origin: https://app.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization
# Server responds with permissions
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 86400
Common CORS Vulnerabilities
1. Wildcard with Credentials
# [ERROR] VULNERABLE - does not work and is dangerous
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
# Browsers block this combination
# [OK] CORRECT - specific origin with credentials
Access-Control-Allow-Origin: https://trusted.com
Access-Control-Allow-Credentials: true
2. Reflection Attack
# [ERROR] VULNERABLE - reflects any origin
const origin = request.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
# [OK] CORRECT - whitelist validation
const allowedOrigins = ['https://app.com', 'https://admin.com'];
const origin = request.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
3. Subdomain Wildcard
# [ERROR] VULNERABLE - poorly implemented regex
const origin = request.headers.origin;
if (/https:\/\/.*\.example\.com/.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
// Accepts https://evil.example.com.attacker.com
# [OK] CORRECT - strict validation
const origin = request.headers.origin;
if (/^https:\/\/[a-z0-9-]+\.example\.com$/.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
Secure Configuration by Technology
Node.js/Express (CORS middleware)
const cors = require('cors');
// Secure configuration
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
if (!origin || allowedOrigins.includes(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
};
app.use(cors(corsOptions));
Nginx
# Conditional configuration
map $http_origin $cors_origin {
default "";
"~^https://app\\.example\\.com$" $http_origin;
"~^https://admin\\.example\\.com$" $http_origin;
}
server {
location /api {
if ($cors_origin != "") {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
}
if ($request_method = OPTIONS) {
return 204;
}
}
}
Apache
# .htaccess
SetEnvIf Origin "^https://(app|admin)\\.example\\.com$" CORS_ORIGIN=$0
Header always set Access-Control-Allow-Origin "%e" env=CORS_ORIGIN
Header always set Access-Control-Allow-Credentials "true" env=CORS_ORIGIN
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE" env=CORS_ORIGIN
Header always set Access-Control-Allow-Headers "Authorization, Content-Type" env=CORS_ORIGIN
# Respond to OPTIONS preflight
RewriteEngine On
RewriteCond % OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
Testing CORS
# Test with curl
curl -H "Origin: https://evil.com" \\
-H "Access-Control-Request-Method: DELETE" \\
-H "Access-Control-Request-Headers: Authorization" \\
-X OPTIONS \\
https://api.example.com/resource
# JavaScript test
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
}).then(response => console.log(response));
Best Practices
- Never use the wildcard (*) in APIs with sensitive data
- Explicit whitelist of allowed origins
- Strict validation of origin with secure regex
- Minimize credentials: Only enable if truly necessary
- Least privilege: Allow only the necessary methods and headers
- Cache preflight: Use Max-Age to reduce overhead
- Monitoring: Log suspicious CORS rejections
Secure CORS Checklist
- [OK] Origin validated against an explicit whitelist
- [OK] Validation regex does not allow bypasses
- [OK] Credentials only enabled when necessary
- [OK] Methods and headers restricted to the minimum
- [OK] Preflight configured correctly
- [OK] Tested against malicious origins
- [OK] Logs of rejected requests monitored
