TL;DR
- Cursor and Claude Code default to
cors()with no arguments in every Express scaffold - Wildcard CORS lets any origin read your API responses -- combine it with credentials and you have CWE-942
- Fix: swap the wildcard for an explicit origin allowlist
I've reviewed maybe a hundred AI-generated Express backends in the last few months. CORS is misconfigured in almost every single one. Not subtly -- the default Cursor scaffold drops app.use(cors()) right below the imports and moves on. No allowlist. No origin validation. Just a wildcard.
For localhost demos this is fine. In production with any authentication layer, it's a problem.
The thing about cors() with no arguments is that it sets Access-Control-Allow-Origin: *. That alone isn't catastrophic -- browsers block credentialed cross-origin requests when the response includes a wildcard origin. So developers hit a CORS error in the browser, paste the error into Cursor, and Cursor "fixes" it by adding credentials: true. Now you have a genuine CWE-942: any origin can make a credentialed request and read your API response.
The Vulnerable Pattern
Cursor generates this at least half the time I scaffold a new route:
const cors = require('cors');
app.use(cors()); // CWE-942: Permissive Cross-Origin Resource Sharing
Or after the inevitable "why is CORS broken" follow-up:
app.use(cors({ origin: '*', credentials: true })); // invalid -- browser rejects wildcard + credentials
Which still breaks. So the next "fix" Cursor often suggests is reflecting the request origin:
app.use(cors({ origin: true })); // reflects any origin as-is -- completely open
That one works in the browser. It also means any site can read your API response after auth. Both the vulnerable pattern and the "fixed" version come from training data -- StackOverflow answers and tutorial code written to make the demo work, not to be secure.
Why This Keeps Happening
The GitHub corpus is full of Express tutorials where cors() with no args is the first line after app.listen. Optimized for "works in 30 seconds on localhost". That's the code AI editors absorbed.
CORS is also confusing enough that developers often don't push back on the AI's suggestion. The browser error message is opaque, the fix looks small, and when it "works" nobody asks what changed. The result is that CWE-942 ends up in codebases with real authentication, real user data, and nobody who remembers why the CORS config looks the way it does.
The Fix
Replace the wildcard with an explicit allowlist:
const allowedOrigins = [
'https://app.yourdomain.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null,
].filter(Boolean);
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
}));
The !origin check passes server-to-server requests and curl where there's no Origin header. Everything else has to be on the list. credentials: true is safe here because origin is validated before that setting applies.
For FastAPI:
# before (CWE-942)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True)
# after
origins = [os.getenv("FRONTEND_URL"), "https://app.yourdomain.com"]
app.add_middleware(
CORSMiddleware,
allow_origins=[o for o in origins if o],
allow_credentials=True
)
Catch it before code review with a semgrep rule:
rules:
- id: wildcard-cors
patterns:
- pattern: cors()
- pattern: cors({ ..., origin: '*', ... })
message: "Wildcard CORS (CWE-942). Use an explicit origin 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 and gitleaks will catch most of what's in this post. The important thing is catching it early, whatever tool you use.
Top comments (0)