CORS set to allow everything, with credentials on
What you'd see: You have app.use(cors()) or an origin reflected from the request header, plus cookie-based sessions.
§ 01 — What's actually happening
The browser blocks cross-site reads by default; CORS is how you selectively unblock them. Allowing every origin while also allowing credentials tells the browser that any website may make authenticated requests to your API using your users' cookies and read the answer. A page a user visits in another tab can quietly act as them.
Why an AI tool writes it this way
CORS errors are among the most frustrating things to debug, and the fastest fix that makes them disappear is to allow everything. It works instantly, and the browser stops complaining — so there is no signal that anything is wrong.
What it costs you
A malicious or merely compromised site can read your users' data and perform actions as them, with no phishing and no stolen password. It is the classic setup for cross-site request forgery against a JSON API.
§ 02 — Before & after
Any website can call your API as your user
Illustrative code, written for this page — never a client's project.
AI-generated
- Every origin allowed, including ones that do not exist yet
- Credentials permitted, so session cookies ride along
- Any method accepted, including DELETE
Human-reviewed
- Explicit allowlist of origins you control
- Unknown origins rejected outright
- Only the methods and headers you actually use
§ 03 — Check your own
How to tell in two minutes
You don't need us to run these. If any of them come back the wrong way, you have this problem.
- Run
curl -I -H "Origin: https://evil.example" https://yourapi.com/api/me. IfAccess-Control-Allow-Origincomes back ashttps://evil.example, every origin is allowed. - Check whether
Access-Control-Allow-Credentials: trueis also present. The two together are what makes this exploitable. - A literal
*wildcard is safer than reflection — browsers refuse to send credentials with it. Reflection is the dangerous pattern. - Set your session cookies to
SameSite=LaxorStrictas a second layer.
Found it in your project? Fixing this one properly usually takes an engineer under an hour. Finding the other nine takes longer — which is what the free health check is for.
Get my free health check →§ 04 — Related
Other things we find
Admin routes protected only by hiding the button
Your admin area is protected by something like {user.isAdmin && <AdminPanel />}.
Records fetched by ID with no ownership check (IDOR)
Endpoints look like findUnique({ where: { id } }) with the ID taken straight from the URL.
Stripe webhook accepts unverified events
Your webhook route reads the request body and acts on it directly.
Read the fix →