CORS Misconfiguration: How It Happens and How to Fix It
CORS misconfigurations let attackers steal data from authenticated users. Learn how wildcard origins, null origins, and regex mistakes create vulnerabilities — and how to configure CORS correctly.
CORS (Cross-Origin Resource Sharing) misconfigurations are a common vulnerability in APIs and web applications. A misconfigured CORS policy can allow an attacker's website to make authenticated requests to your API and read the responses — effectively stealing any data your logged-in users can access.
How CORS Works
By default, browsers block cross-origin requests made by JavaScript. This is the Same-Origin Policy: a script at app.com can't fetch data from api.com.
CORS is a browser mechanism that lets servers relax this restriction for specific origins. When a server responds with:
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Credentials: true
...the browser allows the app.com JavaScript to read the response from api.com.
The key insight: CORS is enforced by the browser, not the server. The request still reaches your server — the browser just decides whether to give the response to the requesting JavaScript.
Dangerous CORS Patterns
1. Wildcard origin with credentials
The most obviously broken configuration:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Browsers actually block this combination — they won't send credentials (cookies, auth headers) with wildcard origin requests. But many developers then "fix" this by dynamically reflecting the Origin header:
// ❌ Dangerous: reflects any origin
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Credentials', 'true');
next();
});
This is equivalent to Access-Control-Allow-Origin: * with credentials enabled — any website can make authenticated requests to your API.
Attack scenario:
- Attacker hosts
evil.comwith JavaScript that fetcheshttps://yourapp.com/api/account - The victim visits
evil.comwhile logged in to your app - The browser sends the request with the victim's cookies
- Your server reflects
Origin: https://evil.comand allows it - The attacker's JavaScript reads the victim's account data
2. Misconfigured regex
A common attempt to whitelist multiple origins uses regex matching:
// ❌ Misconfigured regex
const allowedOrigin = /yourapp\.com/.test(origin);
This matches yourapp.com — but also evil-yourapp.com, yourapp.com.evil.com, and yourapp.company. The regex is too loose.
// ✅ Correct regex
const allowedOrigin = /^https:\/\/(www\.)?yourapp\.com$/.test(origin);
3. Trusting the null origin
Some servers whitelist the null origin for development convenience:
if (origin === 'null') {
res.header('Access-Control-Allow-Origin', 'null');
}
The null origin is sent by sandboxed iframes, file:// URLs, and certain redirects. An attacker can send a request with null origin from a sandboxed iframe on their site and bypass your CORS policy.
Never trust null in a production CORS policy.
4. Broad subdomain wildcards
Access-Control-Allow-Origin: https://*.yourapp.com
If any subdomain is compromised (including a decommissioned one), or if you have a subdomain takeover vulnerability, the attacker gets CORS access to your main API.
Be explicit about which subdomains need access.
5. Overly permissive preflight
Access-Control-Allow-Methods: *
Access-Control-Allow-Headers: *
While less dangerous than origin issues, permitting all methods and headers unnecessarily expands your attack surface.
Correct CORS Configuration
Explicit allowlist
const ALLOWED_ORIGINS = new Set([
'https://yourapp.com',
'https://www.yourapp.com',
'https://dashboard.yourapp.com',
]);
function corsMiddleware(req, res, next) {
const origin = req.headers.origin;
if (origin && ALLOWED_ORIGINS.has(origin)) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Vary', 'Origin'); // Critical: tell caches this varies by origin
}
if (req.method === 'OPTIONS') {
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Max-Age', '86400');
return res.sendStatus(204);
}
next();
}
The Vary: Origin header
When you dynamically set Access-Control-Allow-Origin based on the request origin, you must also set Vary: Origin. Without it, CDNs and proxies may cache a response with one origin's CORS headers and serve it to a different origin.
Express with cors package
import cors from 'cors';
const corsOptions = {
origin: (origin, callback) => {
if (!origin || ALLOWED_ORIGINS.has(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
};
app.use(cors(corsOptions));
Next.js API routes
// app/api/data/route.ts
const ALLOWED_ORIGINS = ['https://yourapp.com', 'https://www.yourapp.com'];
export async function GET(request: Request) {
const origin = request.headers.get('origin') ?? '';
const allowed = ALLOWED_ORIGINS.includes(origin);
return Response.json(
{ data: '...' },
{
headers: {
'Access-Control-Allow-Origin': allowed ? origin : '',
'Access-Control-Allow-Credentials': 'true',
Vary: 'Origin',
},
}
);
}
export async function OPTIONS(request: Request) {
const origin = request.headers.get('origin') ?? '';
const allowed = ALLOWED_ORIGINS.includes(origin);
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': allowed ? origin : '',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
Vary: 'Origin',
},
});
}
Testing CORS Configuration
# Test a specific origin
curl -H "Origin: https://evil.com" \
-H "Cookie: session=yourtoken" \
-v https://yourapi.com/api/account 2>&1 | grep -i "access-control"
# Test preflight
curl -X OPTIONS \
-H "Origin: https://yourapp.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-v https://yourapi.com/api/data
Public APIs vs Authenticated APIs
If your API is public (no authentication, no sensitive data):
Access-Control-Allow-Origin: *
This is fine. Anyone can read the data anyway.
If your API is authenticated (cookies, session tokens, or any user-specific data):
- Never use
* - Always use an explicit allowlist
- Always add
Access-Control-Allow-Credentials: trueonly with specific origins - Always add
Vary: Origin