TL;DR
- Cursor consistently generates
cors()with no config -- equivalent toorigin: '*' - When devs "fix" the credentials error, they often land on
origin: truewhich is worse - Replace with an explicit origin allowlist -- 10 minutes, prevents any site from making credentialed requests to your API
I've been auditing side projects lately. Mostly Node/Express backends, mostly built with Cursor or Claude Code. One pattern shows up without fail: the CORS setup.
Here's what nearly every AI-generated Express app looks like at the top of the entry file:
const cors = require('cors');
app.use(cors());
The AI is not wrong to add CORS. Without it, browsers block cross-origin requests and the frontend breaks immediately. So the AI adds it. The version that works instantly. The version that also lets any website on the internet make requests to your API.
The vulnerable pattern (CWE-942)
// What Cursor generates by default
app.use(cors()); // equivalent to { origin: '*' }
// Or the explicit version
app.use(cors({ origin: '*', credentials: true }));
That second variant is rejected by browsers -- the Fetch spec does not allow * with credentials. So the developer sees the console error, googles it, and lands here:
// "Fixed" after seeing browser console error
app.use(cors({ origin: true, credentials: true }));
origin: true tells the CORS middleware to echo back whatever Origin header the request sends. Combined with credentials: true, any website can make authenticated requests to your API -- with cookies, Authorization headers, session tokens -- and read the response. That's the actual exploit.
Why the AI keeps doing this
Training data for AI editors is saturated with quick-start tutorials, Stack Overflow answers, and "get it working" repos. These all prioritize solving the immediate problem -- the browser blocking cross-origin requests -- not the security consequences.
cors() with no arguments is the fastest path to a working frontend. The AI has learned that this is what developers want. It's not wrong about that. It's just missing the follow-up step that no tutorial bothers to cover.
The fix
Replace the catch-all with an explicit allowlist:
const allowedOrigins = [
'https://yourapp.com',
'https://www.yourapp.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 handles server-to-server requests and curl (which do not send an Origin header). Everything else gets validated against the list.
For multi-tenant apps or dynamic subdomains, use a pattern:
const originPattern = /^https:\/\/([\w-]+\.)?yourapp\.com$/;
origin: (origin, callback) => {
if (!origin || originPattern.test(origin)) callback(null, true);
else callback(new Error('Not allowed by CORS'));
}
Checking if you're already exposed
curl -H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: GET" \
-X OPTIONS https://yourapi.com/api/users -i | grep -i "access-control"
If you see Access-Control-Allow-Origin: https://evil.com in the response -- and especially if you also see Access-Control-Allow-Credentials: true -- fix this before the next deploy.
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 of what's in this post. The important thing is catching it early, whatever tool you use.
Top comments (0)