CORS: The Security Feature Everyone Hates (Until They Get Hacked) 🌐
CORS: The Security Feature Everyone Hates (Until They Get Hacked) 🌐
So you're building an API and suddenly your browser throws: "Access blocked by CORS policy"
Your first thought? "CORS is broken, let me Google how to disable it" 🤦♂️
Stop right there! You're about to create a security hole big enough to drive a truck through. Let me save you from yourself.
What Even Is CORS? (The 2-Minute Explanation) 🤔
CORS = Cross-Origin Resource Sharing
Translation: Rules that control which websites can talk to your API.
Think of it like a nightclub:
- Without CORS: Random websites walk in and steal your API data
- With CORS: Bouncer checks IDs - "You're on the list? Cool. You're not? Get out."
The key point: CORS is a browser security feature, not a bug. Your browser is trying to protect your users!
Why Browsers Block Cross-Origin Requests 🚫
The scenario without CORS:
- You're logged into
yourbank.com - You visit
evil-site.com - Evil site's JavaScript makes a request to
yourbank.com/api/transfer-money - Your browser automatically includes your bank cookies
- Money goes bye-bye 💸
With CORS: Browser says "Wait, does yourbank.com allow evil-site.com to make requests? No? BLOCKED."
Real Talk: CORS prevents websites from silently stealing data from APIs where you're authenticated. It's literally protecting you!
The Mistakes That Get You Hacked 💣
Mistake #1: The Nuclear Option (Access-Control-Allow-Origin: *)
// The "fix" every Stack Overflow answer suggests
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*'); // DANGER!
res.header('Access-Control-Allow-Methods', '*'); // STOP!
res.header('Access-Control-Allow-Headers', '*'); // WHY!
next();
});
What this means: "Every website in the entire universe can access my API!" 🌍
The problem:
- Evil sites can now call your API
- If your API returns user data, it's now public
- Credentials won't work with
*(but you're already screwed if you're using this)
When * is actually okay:
- Truly public APIs (weather data, public blog posts, etc.)
- No authentication required
- No user-specific data
Translation: If your API requires login or returns personalized data, * is a terrible idea.
Mistake #2: Reflecting the Origin (The "Smart" Bad Idea)
// "I'm clever!" (Narrator: They were not)
app.use((req, res, next) => {
const origin = req.headers.origin;
res.header('Access-Control-Allow-Origin', origin); // Just as bad!
res.header('Access-Control-Allow-Credentials', 'true');
next();
});
What you think: "Only the requesting origin can access it!"
What actually happens: You just allowed EVERY origin. Evil sites included!
Why it's dangerous:
Access-Control-Allow-Credentials: truemeans cookies are included- Evil site makes request → Gets reflected as allowed → Steals user data
- Congrats, you just reinvented the vulnerability CORS was designed to prevent
Mistake #3: Not Validating Origins Properly
// BAD: Substring matching
const origin = req.headers.origin;
if (origin && origin.includes('mysite.com')) {
res.header('Access-Control-Allow-Origin', origin);
}
The exploit: Attacker registers mysite.com.evil-hacker.ru → Passes your check! 🎭
Or: evil-mysite.com, mysite.com.attacker.xyz, etc.
Real Talk: Origin validation is like comparing passwords - close enough doesn't count!
The Right Way to Handle CORS ✅
Option 1: Whitelist Specific Origins (The Safe Default)
// Node.js with Express
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com',
'https://admin.myapp.com',
'http://localhost:3000' // For development
];
app.use((req, res, next) => {
const origin = req.headers.origin;
// EXACT match only!
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}
// Handle preflight requests
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
Why this works:
- Exact match only - no substring tricks
- Only trusted origins get access
- Credentials (cookies) work safely
- Clear list of who's allowed
Pro Tip: Use environment variables for different environments!
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
Option 2: Using the cors Package (Even Better)
const cors = require('cors');
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com'
];
// Allow requests with no origin (mobile apps, Postman)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true, // Allow cookies
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400 // Cache preflight for 24 hours
};
app.use(cors(corsOptions));
Benefits:
- Battle-tested library
- Handles preflight automatically
- Clear error messages
- Less code to screw up
Option 3: Different Policies for Different Endpoints
// Public endpoints: Allow everyone
app.get('/api/public/posts',
cors({ origin: '*' }),
getPosts
);
// Protected endpoints: Strict whitelist
const restrictedCors = cors({
origin: ['https://myapp.com', 'https://www.myapp.com'],
credentials: true
});
app.get('/api/user/profile',
restrictedCors,
authenticate, // Require auth
getProfile
);
app.post('/api/user/update',
restrictedCors,
authenticate,
updateProfile
);
Translation: Public stuff gets *, user data gets locked down!
Understanding Preflight Requests 🛫
The weirdness: Browser sends an OPTIONS request before your actual request.
Why it happens: For "complex" requests (anything beyond simple GET/POST with basic headers).
What the browser is asking: "Hey API, is myapp.com allowed to make a POST with custom headers?"
Your API's job: Respond with proper CORS headers.
// Preflight response
app.options('/api/*', (req, res) => {
res.header('Access-Control-Allow-Origin', 'https://myapp.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Max-Age', '86400'); // Cache for 24h
res.sendStatus(204); // No content
});
Performance tip: Use Access-Control-Max-Age to cache preflight responses. No need to check every single time!
The Development vs Production Dance 💃
The problem: You need localhost for dev, but production should be locked down.
The solution:
const allowedOrigins = process.env.NODE_ENV === 'production'
? [
'https://myapp.com',
'https://www.myapp.com'
]
: [
'http://localhost:3000',
'http://localhost:3001',
'https://myapp.com' // Still test prod config
];
Or use wildcards for local only:
origin: function (origin, callback) {
// Production: exact match
if (process.env.NODE_ENV === 'production') {
const allowed = ['https://myapp.com'];
return callback(null, allowed.includes(origin));
}
// Development: allow localhost on any port
if (origin?.match(/^http:\/\/localhost:\d+$/)) {
return callback(null, true);
}
callback(new Error('Not allowed by CORS'));
}
Best of both worlds: Easy local development, secure production!
Common CORS Scenarios (And How to Fix Them) 🔧
Scenario 1: "CORS error but Postman works fine!"
Why: Postman doesn't enforce CORS (it's not a browser).
The fix: Configure CORS properly for browsers, not for Postman.
Scenario 2: "Working in dev, broken in production!"
Why: You allowed http://localhost but your prod site is https://myapp.com.
The fix: Use environment-specific origin lists (see above).
Scenario 3: "GET works, POST doesn't!"
Why: POST triggers preflight, your OPTIONS handler is missing.
The fix: Add OPTIONS handler (or use the cors package).
Scenario 4: "CORS works but cookies don't send!"
Why: Need both Access-Control-Allow-Credentials: true AND credentials: 'include' in fetch.
The fix:
// Backend
res.header('Access-Control-Allow-Credentials', 'true');
// Frontend
fetch('https://api.myapp.com/data', {
credentials: 'include', // Send cookies!
headers: { 'Content-Type': 'application/json' }
});
The Security Checklist 📋
Before you deploy:
- NOT using
Access-Control-Allow-Origin: *for protected endpoints - Using exact origin matching (not substring)
- Only whitelisting domains you actually own/trust
-
Access-Control-Allow-Credentialsonly with specific origins - Handling OPTIONS preflight requests
- Different CORS policies for public vs protected endpoints
- Environment-specific origin lists
- Caching preflight with
Access-Control-Max-Age - Testing with actual browsers (not just Postman!)
Quick Wins (Fix Your CORS Today!) 🏃♂️
Node.js/Express:
npm install cors
Laravel:
php artisan config:publish cors
# Edit config/cors.php
Django:
pip install django-cors-headers
# Add to MIDDLEWARE
FastAPI:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myapp.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Real Talk 💬
Q: "Can I just disable CORS?"
A: No. It's a browser feature, not something you can disable server-side. You can misconfigure it to allow everything (which is basically the same as disabling it and equally stupid).
Q: "Why does my API work in Postman but not the browser?"
A: Postman isn't a browser, so CORS doesn't apply. This is by design!
Q: "What about mobile apps?"
A: Mobile apps don't enforce CORS (it's browser-specific). But you should still secure your API with proper authentication!
Q: "Is CORS authentication?"
A: NO! CORS controls which origins can access your API. You still need proper authentication (JWT, sessions, etc.) to control who can access it!
The Bottom Line
CORS exists for a reason: to protect your users from malicious websites stealing their data.
When you see a CORS error, don't think "how do I disable this annoying thing?" Think "how do I configure this security feature properly?"
The essentials:
- Whitelist specific origins (exact matches only!)
- Never reflect the Origin header without validation
- Use
*only for truly public APIs (no auth, no user data) - Handle preflight OPTIONS requests properly
- Test with actual browsers, not just Postman
Think of CORS like a seatbelt - annoying when you first get in the car, but you'll be really glad it's there when you need it! 🚗💨
Still confused about CORS? Hit me up on LinkedIn - I've debugged enough CORS issues to write a book!
Want more security content? Check out my GitHub and follow this blog!
P.S. - If you're reading this and you have Access-Control-Allow-Origin: * with credentials enabled, go fix that RIGHT NOW. I'll wait. 🌐✨