CORS is not a security mechanism — it is a controlled relaxation of the Same-Origin Policy. Every misconfiguration makes your API more permissive, not less.
What CORS Actually Does
The Same-Origin Policy prevents evil.com from making JavaScript requests to yourapi.com and reading the response. CORS (Cross-Origin Resource Sharing) creates exceptions to this rule.
When a browser makes a cross-origin request, the server responds with headers that say "I allow requests from this origin." If the headers are missing or wrong, the browser blocks the response.
GET /api/user HTTP/1.1
Host: api.yourapp.com
Origin: https://yourapp.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://yourapp.com
The critical thing to understand: CORS is enforced by the browser, not the server. The server always processes the request and sends the response. The browser just decides whether JavaScript can read that response.
The 5 Dangerous Misconfigurations
1. Wildcard with Credentials
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
This is the most common mistake. Browsers actually block this combination — Access-Control-Allow-Origin: * cannot be used with credentials: true. But developers "fix" this by reflecting the origin instead (see #2), which is worse.
What to do: Never use * if your API uses cookies, sessions, or any form of credentials. Whitelist specific origins.
2. Reflecting the Origin Header
// ❌ Extremely dangerous
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
next();
});This mirrors whatever origin the browser sends — including https://evil.com. An attacker's site can now make authenticated requests to your API and read the responses, stealing user data.
What to do: Validate the origin against a whitelist:
const ALLOWED_ORIGINS = [
"https://yourapp.com",
"https://staging.yourapp.com",
];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (ALLOWED_ORIGINS.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Vary", "Origin");
}
next();
});3. Trusting null Origin
// ❌ Dangerous — null origin can be forged
if (origin === "null" || ALLOWED_ORIGINS.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
}The null origin comes from sandboxed iframes, local files, and redirects. An attacker can craft a sandboxed iframe that sends requests with Origin: null:
<iframe sandbox="allow-scripts" src="data:text/html,
<script>
fetch('https://api.yourapp.com/user', {credentials: 'include'})
.then(r => r.json())
.then(d => parent.postMessage(d, '*'));
</script>
"></iframe>What to do: Never whitelist null as an allowed origin.
4. Missing Vary: Origin Header
When your CORS response depends on the Origin header, you must include Vary: Origin. Without it, CDNs and browser caches may serve the wrong CORS headers:
# Request from app-a.com gets cached
Access-Control-Allow-Origin: https://app-a.com
# Later request from app-b.com gets the cached response
# Browser sees app-a.com in the header, blocks app-b.com
What to do: Always include Vary: Origin when the Access-Control-Allow-Origin value changes per request:
res.setHeader("Vary", "Origin");5. Overly Permissive Preflight
Preflight requests (OPTIONS) check whether the actual request is allowed. Some developers cache preflight responses for too long or allow too many methods and headers:
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
Access-Control-Allow-Headers: *
Access-Control-Max-Age: 86400
What to do: Only allow the methods and headers your API actually uses:
res.setHeader("Access-Control-Allow-Methods", "GET, POST");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Max-Age", "600"); // 10 minutes, not 24 hoursProper CORS Configuration
Here is a secure CORS setup for Express:
import cors from "cors";
const corsOptions = {
origin: (origin, callback) => {
const whitelist = [
"https://yourapp.com",
"https://staging.yourapp.com",
];
if (!origin || whitelist.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
credentials: true,
methods: ["GET", "POST"],
allowedHeaders: ["Content-Type", "Authorization"],
maxAge: 600,
};
app.use(cors(corsOptions));When to Use Wildcard
Access-Control-Allow-Origin: * is fine when:
- Your API is completely public (no authentication)
- You serve static assets (fonts, public images)
- You are a public CDN
It is not fine when your API uses cookies, tokens, or any form of session.
Testing Your CORS Configuration
# Test a cross-origin request
curl -H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: GET" \
-I https://api.yourapp.com/user
# Check if the response reflects the evil origin
# If Access-Control-Allow-Origin: https://evil.com appears, you are vulnerableConclusion
CORS misconfigurations are consistently in the OWASP Top 10 for API security issues. The rules are simple: never reflect arbitrary origins, never trust null, always set Vary: Origin, and whitelist only the domains that need access. Test with curl from an untrusted origin before you ship.
Related posts
Content Security Policy: A Practical CSP Implementation Guide for Developers
Learn how to implement Content Security Policy headers correctly, avoid common misconfigurations, and stop XSS attacks before they reach your users.
Mar 30, 2026 · 9 min readPrototype Pollution in JavaScript: How __proto__ Can Break Your App
Learn how prototype pollution works in JavaScript, review real-world CVEs in lodash and jQuery, and implement defenses to prevent this class of vulnerability.
Mar 29, 2026 · 7 min read