TL;DR
- AI editors keep generating CORS middleware that reflects the request's Origin header back verbatim while also allowing credentials, so any site on the internet can make authenticated calls to your API and read the response
- It passes local testing because reflecting the origin "just works" for every dev port and tool, so nobody notices until a scanner or a pentest catches it (CWE-942)
- The fix is a hardcoded allowlist checked against each request's Origin, never a reflection of whatever the browser sends
I hit a CORS error in a side project last week. Frontend on one port, API on another, browser blocking the request. I asked Cursor to fix it, and it did, instantly. The error went away. I moved on to the actual feature I was building.
Three days later I was reading through the generated middleware for something unrelated and noticed what "fixed" actually meant. It hadn't allowlisted my frontend's origin. It had told the server to accept requests from anywhere, with cookies attached.
That's the trade AI editors make by default. You ask for the error to go away, and the fastest way to make a CORS error go away is to stop checking who's asking.
The Vulnerable Code
The pattern shows up as origin reflection, not a plain wildcard. A bare Access-Control-Allow-Origin: * combined with Access-Control-Allow-Credentials: true actually gets rejected by the browser, so tutorials work around that by echoing back whatever Origin header the request sent. That workaround is the vulnerability.
// ❌ CWE-942: Permissive Cross-domain Policy
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
next();
});
req.headers.origin is a value the requester controls. A page hosted on evil-site.com sends Origin: evil-site.com, the server echoes it straight back, and the browser sees a match. Credentials are allowed, so the victim's session cookie rides along. Any authenticated GET or POST your API exposes is now readable and callable from a page the victim never meant to trust.
Why This Keeps Happening
Origin reflection survives in training data because it's the fastest fix that makes a CORS error disappear in local development, and almost every public snippet optimizes for that. Stack Overflow answers, boilerplate repos, and "quick fix" gists all reach for the same three lines because they work identically whether you're testing on localhost:3000, localhost:5173, or a Vercel preview URL. Nobody has to think about which origins are actually supposed to be allowed.
An explicit allowlist requires knowing your production frontend's domain ahead of time, which isn't information available in an isolated code snippet. So the training data skews toward the version that works everywhere and restricts nothing. AI editors reproduce that pattern faithfully, and it ships because it passes every test a developer actually runs before deploying.
The Fix
Replace origin reflection with a fixed list of domains you actually trust, checked explicitly against each incoming request.
// ✅ Explicit allowlist, no reflection
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
Python/Flask version of the same fix:
from flask_cors import CORS
CORS(
app,
origins=["https://app.example.com", "https://admin.example.com"],
supports_credentials=True
)
For local development, add localhost ports to the allowlist explicitly rather than falling back to a wildcard or reflection "just for now." Environment variables work well here: one allowlist for dev, a locked-down one for production, both explicit.
FAQ
Q: Is Access-Control-Allow-Origin: * with credentials actually dangerous?
A: Browsers block that exact combination outright, which is why the real-world bug is origin reflection instead. Reflecting the request's Origin header back while allowing credentials produces the same result the wildcard was trying to achieve, and it isn't blocked.
Q: Does a CORS allowlist replace authentication?
A: No. CORS controls which browser-based origins can read a response, not who's authorized to call the endpoint. You still need real auth checks; CORS is a second layer that stops the browser from letting an untrusted page use a victim's session.
Q: How do I check my API for this right now?
A: Look for any CORS middleware where the Access-Control-Allow-Origin value comes from a request header instead of a fixed list. If you see req.headers.origin or req.get('origin') written straight into the response, that's the pattern.
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. 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)