TL;DR
Whitelist specific origins rather than using wildcards. Access-Control-Allow-Origin: * with credentials is rejected by browsers anyway, but the intent alone is wrong. The trickier risk is dynamic origin reflection: an origin validator that reflects back whatever it receives lets any site access your API with user cookies. CORS protects users, not your server endpoints. Attackers with curl bypass it entirely.
"CORS protects users, not servers. A misconfigured policy lets attackers weaponize your users' browsers against your own API."
What CORS Actually Protects
CORS (Cross-Origin Resource Sharing) is a browser security feature that prevents malicious websites from accessing data from other sites.
CORS protects users, not your API. CORS is enforced by browsers. Attackers can still call your API directly with tools like curl. CORS prevents a malicious site from using a victim's browser to steal their data from your API.
Best Practice 1: Whitelist Specific Origins 3 min
Wildcards are fine for public static files. For any API that handles authentication or user data, they're not:
import cors from 'cors';
// WRONG: Allows any origin
app.use(cors({ origin: '*' }));
// WRONG: Allows any origin with credentials (browser blocks this anyway)
app.use(cors({ origin: '*', credentials: true }));
// CORRECT: Whitelist specific origins
const allowedOrigins = [
'https://yourdomain.com',
'https://app.yourdomain.com',
process.env.NODE_ENV === 'development' && 'http://localhost:3000',
].filter(Boolean);
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, curl, etc.)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
}));
Best Practice 2: Avoid Dynamic Origin Reflection 2 min
This is the one that actually gets apps compromised. Reflecting back whatever origin is sent looks like a flexible whitelist. It isn't:
// DANGEROUS: Reflects any origin
app.use(cors({
origin: (origin, callback) => {
callback(null, origin); // Reflects attacker's origin!
},
credentials: true,
}));
// This allows ANY site to access your API with cookies
// Attacker's site can steal user data
// CORRECT: Validate against whitelist
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, origin);
} else {
callback(new Error('Not allowed'));
}
},
credentials: true,
}));
Best Practice 3: Configure Methods and Headers 2 min
Only allow the methods and headers you actually use. Permissive defaults here aren't dangerous by themselves, but they expand your attack surface for free:
app.use(cors({
origin: allowedOrigins,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'], // Only methods you use
allowedHeaders: ['Content-Type', 'Authorization'], // Only headers you need
exposedHeaders: ['X-Request-Id'], // Headers client can read
maxAge: 86400, // Cache preflight for 24 hours
}));
Best Practice 4: Handle Preflight Requests 3 min
Browsers send a preflight OPTIONS request before certain cross-origin requests. If you're not using a CORS middleware, you need to handle it yourself:
// When not using cors middleware
app.options('*', (req, res) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '86400');
res.status(204).end();
} else {
res.status(403).end();
}
});
Best Practice 5: CORS with Cookies 3 min
Cookies need their own CORS settings on both sides of the request:
// Server: Allow credentials
app.use(cors({
origin: 'https://app.yourdomain.com', // Must be specific, not *
credentials: true, // Allow cookies
}));
// Client: Include credentials in fetch
fetch('https://api.yourdomain.com/user', {
credentials: 'include', // Send cookies
});
// Cookie must also have proper settings
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'none', // Required for cross-origin
domain: '.yourdomain.com', // Shared domain
});
Best Practice 6: Per-Route CORS 2 min
Different routes may need different CORS policies:
import cors from 'cors';
// Public API: Allow any origin, no credentials
const publicCors = cors({
origin: '*',
methods: ['GET'],
});
// Private API: Strict origin, with credentials
const privateCors = cors({
origin: allowedOrigins,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
});
// Apply per route
app.get('/api/public/status', publicCors, statusHandler);
app.use('/api/user', privateCors, userRouter);
app.use('/api/admin', privateCors, adminRouter);
Common CORS Mistakes
| Mistake | Risk | Prevention |
|---|---|---|
| origin: "*" with credentials | Browser blocks (but intent is wrong) | Whitelist specific origins |
| Reflecting any origin | Cross-origin data theft | Validate against whitelist |
| Trusting null origin | Sandbox escape attacks | Never allow null origin |
| Regex subdomain matching | Bypass via evil.com.attacker.com | Use exact matches |
| CORS on public files | Unnecessary (use CDN) | CORS for API only |
External Resources
For comprehensive CORS documentation and specifications, see the MDN CORS documentation, which covers browser behavior, preflight requests, and all CORS headers in detail.
When should I use Access-Control-Allow-Origin: *?
Only for truly public resources that don't require authentication and contain no sensitive data: weather APIs, static files, public documentation. Never with credentials.
Why does CORS not protect my API from hackers?
CORS is enforced by browsers only. Attackers can bypass it using curl, Postman, or their own servers. CORS protects users by preventing malicious sites from using their browser to access your API.
How do I allow multiple origins?
CORS only allows one origin in the header. Use a function to check the request origin against your whitelist and return that origin if allowed. See the examples above.
What is the null origin and should I allow it?
The null origin comes from sandboxed iframes, file:// URLs, and some redirects. Allowing it can enable attacks from sandboxed pages. Don't allow it unless you have a specific, understood reason.
Further Reading
Put these practices into action with our step-by-step guides.
Check Your CORS Configuration
Scan your API for CORS misconfigurations.