CORS misconfigurations are one of the easiest ways to accidentally expose authenticated APIs to attacker-controlled origins. They rarely show up as obvious bugs — your app keeps working, your CI passes, but somewhere a Access-Control-Allow-Origin: * header is quietly handing data to anyone who asks. This post walks through the specific misconfigurations we see most often when scanning sites with WebSentry, and exactly how to fix each one.
What CORS actually protects (and what it doesn't)
CORS is not a security feature for your server. It's a browser-enforced relaxation of the Same-Origin Policy. When you set Access-Control-Allow-Origin, you're telling the browser: "It's OK to let JavaScript from this other origin read my response." If you get that wrong, any malicious site a logged-in user visits can make authenticated requests to your API and read the response.
Three things matter when auditing CORS:
- Which origins are allowed
- Whether credentials (cookies, Authorization headers) are allowed
- Which methods and headers are exposed
The seven misconfigurations and how to fix them
1. Wildcard origin on an authenticated endpoint
The classic mistake: returning Access-Control-Allow-Origin: * on an endpoint that uses cookies or tokens.
Fix: Never combine * with credentials. Maintain an allowlist and echo the matched origin:
const ALLOWED = new Set([
'https://app.example.com',
'https://admin.example.com'
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && ALLOWED.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
next();
});
2. Reflecting any origin without validation
This pattern looks safe but isn't:
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
Any origin — including https://evil.com — gets reflected back. Combined with credentials, this is a full CORS bypass.
Fix: Always validate against an allowlist before echoing. Use exact string matches, not startsWith or includes.
3. Sloppy subdomain regex
Patterns like /example.com$/ match evilexample.com. Patterns like /.*.example.com/ match example.com.attacker.net.
Fix: Anchor your regex and escape dots explicitly:
const SUBDOMAIN_RE = /^https://([a-z0-9-]+.)?example.com$/;
Better still, parse the origin with URL() and check the hostname against a list of suffixes.
4. null origin allowed
Browsers send Origin: null for sandboxed iframes, redirects from file://, and data URLs. If your allowlist contains the string "null", attackers can trigger requests from sandboxed iframes they control.
Fix: Remove null from any allowlist. There's almost never a legitimate reason to allow it for an authenticated API.
5. Trusting the Origin header on the server
CORS headers tell the browser what's allowed. They don't authenticate the request. Don't use Origin for authorization decisions on the server — a non-browser client can send any value.
Fix: Authenticate with tokens, session cookies with SameSite=Lax or Strict, and CSRF tokens for state-changing requests. CORS is one layer; it's not the whole stack.
6. Missing Vary: Origin
If you dynamically set Access-Control-Allow-Origin based on the request, you must add Vary: Origin. Otherwise CDNs and browser caches may serve a response intended for one origin to another origin entirely.
Fix: Always pair dynamic ACAO with Vary: Origin. Check your CDN (Cloudflare, Fastly, CloudFront) respects it — some require explicit configuration.
7. Overly permissive preflight responses
Preflight (OPTIONS) responses often get copy-pasted with everything wide open:
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
Access-Control-Allow-Headers: *
Access-Control-Max-Age: 86400
Fix: Only list methods and headers you actually accept. If your API only does GET and POST with Content-Type and Authorization, say so:
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
A safe CORS configuration template
Here's a baseline that works for most authenticated APIs:
- Maintain an explicit allowlist of origins per environment
- Validate
Originagainst the allowlist with exact matches - Echo the matched origin in
Access-Control-Allow-Origin - Set
Access-Control-Allow-Credentials: trueonly if you need cookies or auth headers - Set
Vary: Originon every dynamic response - Restrict methods and headers in preflight to the minimum needed
- Set a short
Access-Control-Max-Age(300–600 seconds) during rollout, raise once stable - Never include
nullor wildcards in production
Testing your fix
Once you've made changes, verify them — don't trust that the code does what you think it does.
Manual checks with curl
Simulate a cross-origin preflight:
curl -i -X OPTIONS https://api.example.com/users -H "Origin: https://evil.com" -H "Access-Control-Request-Method: GET"
You should see no Access-Control-Allow-Origin header in the response. Repeat with an allowed origin and confirm the headers come back correctly.
Edge cases to test
Origin: nullOrigin: https://example.com.evil.comOrigin: https://evilexample.comOrigin: http://example.com(wrong scheme)- Trailing slash, uppercase, port variations
Automated scanning
Manual testing catches the obvious cases. For ongoing coverage, run a scanner that checks header behaviour across multiple origins. WebSentry tests for reflected origins, credentialed wildcards, null-origin acceptance, and missing Vary headers, and grades the result alongside your CSP, cookie flags, and SSL configuration.
Framework-specific notes
Express
The cors package accepts a function for origin — use it rather than passing a static array, so you can return a 403 for unknown origins instead of silently omitting the header.
Nginx
Avoid add_header Access-Control-Allow-Origin $http_origin. Use a map block to translate allowed origins into a validated value, and remember add_header doesn't inherit into nested locations.
Cloudflare Workers / edge
Set CORS at the edge if you have multiple origin servers, but ensure the edge logic mirrors your application's allowlist exactly. Drift between the two is a common source of bugs.
Run a scan
If you're not sure whether your current CORS setup is safe, run a free scan at websentry.dev — you'll get a graded report covering CORS along with the other headers and TLS settings that usually fail together.
Check your own site
Run a free security scan and see if your site has the issues covered in this article. Results in under 30 seconds.