TL;DR
- AI editors almost always default to
cors()with no config -- which setsAccess-Control-Allow-Origin: * - Wildcard CORS on authenticated APIs exposes your users to cross-site request attacks
- Fix: replace the wildcard with an explicit origin allowlist controlled by an env var
I was reviewing a side project a dev built entirely in Cursor. The Express backend looked clean -- structured routes, solid error handling, decent auth middleware. Then I checked the CORS setup.
app.use(cors()); // defaults to Access-Control-Allow-Origin: *
One line. Auto-suggested by Cursor from a starter template. Left in production because it made the frontend stop complaining in dev. Except this was prod.
Wildcard CORS feels harmless compared to SQL injection. No immediate data breach. But for any API using cookies or session tokens, a wildcard CORS config means any website -- a phishing page, a malicious ad iframe -- can make authenticated requests on behalf of your logged-in users without them knowing.
The vulnerable pattern (CWE-942)
Here's what Cursor and Claude Code almost always suggest when you ask for a working Express backend:
// CWE-942: Permissive Cross-domain Policy
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors()); // Allows all origins -- no questions asked
app.use(express.json());
app.get('/api/user/profile', authenticate, (req, res) => {
res.json(req.user);
});
The cors() call with no arguments sets Access-Control-Allow-Origin: * on every response. If your frontend then adds credentials: true to fetch calls, modern browsers will block the request and throw a CORS error. At that point developers often "fix" it like this:
// Even worse
app.use(cors({ origin: '*', credentials: true }));
Browsers block this combination too (you cannot use wildcard origin with credentials). But the intent matters: developers are being guided step by step toward maximally permissive configs, just to silence console errors.
Why AI editors keep generating this
Training data skews toward "this works" not "this is safe." Hundreds of tutorials, Stack Overflow answers, and README files show app.use(cors()) as the one-liner to get your frontend talking to your backend. That's what the model learned. When Cursor generates a backend starter, it's optimizing for immediate functionality -- wildcard CORS delivers that, so it gets generated.
The security implications don't surface until much later, if at all. Most projects never get a security review.
The actual fix
Replace the wildcard with an explicit origin allowlist. Keep the list in environment variables so staging and production configs stay separate:
// Explicit origin allowlist
const allowedOrigins = process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(',')
: [];
app.use(cors({
origin: (origin, callback) => {
// Allow server-to-server requests (no Origin header)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS blocked: ${origin}`));
}
},
credentials: true // safe -- origin is explicitly verified before this applies
}));
Your .env:
ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
If you're not using the cors library:
// Manual headers -- explicit and auditable
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
The critical difference: you're echoing back the verified origin, not broadcasting a wildcard. Every CORS request gets an explicit answer.
One more thing -- add this to your semgrep config to catch wildcard CORS before it reaches code review:
rules:
- id: cors-wildcard
patterns:
- pattern: cors({ ..., origin: '*', ... })
- pattern: cors()
message: Wildcard CORS detected -- replace with explicit allowlist
severity: WARNING
languages: [javascript, typescript]
I've been running SafeWeave for this. It hooks into Cursor and Claude Code as an MCP server and flags these patterns before I move on. That said, even a basic pre-commit hook with semgrep will catch most wildcard CORS configs in seconds. The important thing is catching it early, whatever tool you use.
Top comments (1)
One thing worth adding to the dynamic allowlist pattern: when you echo back the verified origin instead of returning a fixed string, you need
Vary: Originon the response. Without it, a CDN or reverse proxy may cache a response withAccess-Control-Allow-Origin: https://yourdomain.comand serve it to a different origin's preflight - which either breaks legitimate requests or, in misconfigured cache setups, hands an approved CORS header to an origin that shouldn't receive it. Thecorslibrary handles this automatically when you use the function form, but in the manual headers version from the article, it's a one-liner that's easy to miss:res.setHeader('Vary', 'Origin'). Worth adding to the semgrep rule too - flag manual CORS middleware that setsAccess-Control-Allow-Origindynamically without a correspondingVary: Origin.