CORS Misconfigurations That Leak User Data (and How to Test For Them)
AI generators fix CORS errors by making the browser stop complaining, not by making the server safe. Here are the patterns that leak user data · and how to test.
By Gabriel CA · Kraftwire Software
· 9 min readKey takeaway
If your app's backend hands out an `Access-Control-Allow-Origin` header that reflects whatever site made the request · and pairs it with `Access-Control-Allow-Credentials: true` · then any malicious website your logged-in users visit can read their private data straight from your API. AI code generators cause this constantly because the fastest way to silence a CORS error in the console is to make the browser stop complaining, not to make the server actually safe. You cannot catch this from a browser dev-tools tab · it has to be tested server-side with crafted request headers.
First, what the Same-Origin Policy actually protects
The Same-Origin Policy (SOP) is the browser's oldest and most important security boundary. An "origin" is the triple of scheme, host, and port · so `https://app.simplyscan.io` and `https://evil.com` are different origins, and so are `http://` vs `https://` versions of the same host.
By default, SOP lets a page make a cross-origin request, but it stops the calling JavaScript from reading the response. This is the whole reason your login session is safe. When you are signed into your app and you wander over to some random site, that site's JavaScript can fire a `fetch()` at your API · the browser will even attach your cookies · but SOP blocks the attacker's script from ever seeing the JSON that comes back. The data stays sealed.
CORS (Cross-Origin Resource Sharing) is the mechanism that deliberately pokes holes in that seal. When your API responds with the right headers, the browser is told "it is OK to let this specific other origin read my response." Used correctly, that is exactly how your frontend on one domain talks to your API on another. Used carelessly, it is how you hand attackers the keys.
Why AI-generated backends get this wrong
When you are vibe-coding on Lovable, Cursor, Bolt, v0, Replit, or Windsurf, the first CORS error looks like a bug to squash. The console screams `blocked by CORS policy`, and the model's job is to make that message disappear. The path of least resistance is always the most permissive config · reflect the caller's origin, allow all methods, allow all headers, allow credentials. The error vanishes, the demo works, everybody moves on.
The problem is that "the error went away" and "this is secure" are completely different states, and nothing in the local dev loop tells you which one you landed in. The dangerous config and the safe config look identical in your browser. You only see the difference when someone sends a request your frontend would never send.
For the bigger picture on how these permissive defaults stack up across a generated backend, see our [architecture security risks](/blog/architecture-security-risks) post.
The dangerous patterns, concretely
1. Reflecting the request Origin
This is the core mistake. The server reads the incoming `Origin` header and echoes it right back:
Access-Control-Allow-Origin: https://whatever-the-caller-sent.com
It feels safe because the browser stops complaining for your real frontend. But it "works" for every origin · including `https://evil.com` · because the server never actually checks anything. It just mirrors. On its own, reflecting the origin on public, non-credentialed data is bad hygiene. Combined with credentials (below), it is a genuine data-leak vulnerability.
2. Wildcard plus credentials
You will sometimes see this pairing attempted:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Browsers explicitly refuse this combination · a wildcard `*` is not allowed to be used with credentialed requests, and the fetch will fail. So this specific pair is not directly exploitable. But it is a loud signal that whoever wrote the config was reaching for "allow everything" without understanding the model. Wherever you find it, you usually find the reflecting pattern nearby as the "fix" someone applied when the wildcard stopped working.
3. Accepting Origin: null
Some backends allow-list the literal string `null`:
Access-Control-Allow-Origin: null
This looks harmless · like it just handles requests with no origin. But `null` is a real, attacker-reachable origin. Sandboxed iframes, `data:` URLs, and certain redirect chains all send `Origin: null`. An attacker can serve a page from one of those contexts and satisfy your `null` allow-list. Never treat `null` as trusted.
4. The critical one · reflected origin with credentials
Put the reflection together with credentials and you have the real vulnerability:
Access-Control-Allow-Origin: https://evil.com
Access-Control-Allow-Credentials: true
Now the sequence is: your user is logged in, they visit `evil.com`, that page runs `fetch('https://your-api.com/account', { credentials: 'include' })`. The browser attaches the user's cookies, your server reflects `evil.com` back as the allowed origin and says credentials are allowed · and the browser dutifully hands the authenticated JSON response to the attacker's script. Account details, tokens, private records · whatever that endpoint returns. This is the pattern that actually leaks user data, and it is the one AI-generated backends fall into most often.
Why you can't test this from your browser
It is tempting to think you can check this from the dev-tools console on your own site. You can't, and the reasons are the same protections that make SOP work.
The browser will not let JavaScript set the `Origin` header. It is a forbidden header · the browser controls it, so you cannot forge a request that pretends to come from `evil.com`.
Even if a cross-origin response comes back, SOP blocks your script from reading it unless the CORS headers permit your origin. So a "successful" read in the console only tells you about your own origin, not an attacker's.
To actually prove the vulnerability, you have to send the request from outside the browser · a server or tool that can set an arbitrary `Origin` header and then read the raw response headers the server sends back. That is the only way to see whether your API reflects `https://evil.com` and whether it also says `Access-Control-Allow-Credentials: true`.
That is exactly what our free [CORS Tester](/tools/cors-tester) does · it sends crafted `Origin` headers server-side, including a random attacker origin and the `null` origin, and reports back the actual `Access-Control-Allow-Origin` and `Access-Control-Allow-Credentials` values your API returns. No guessing, no console gymnastics. You can find it alongside our other free checks at the [/tools](/tools) hub.
The fix
The correct approach is boringly explicit, and that is the point.
Keep a hard-coded allow-list of the exact origins you trust · your production frontend, your staging domain, maybe `localhost` for dev. Compare the incoming `Origin` against that list.
If it matches, echo back that one specific origin. If it doesn't match, send no CORS headers at all · do not reflect it.
Never reflect an arbitrary origin. Reflection is only safe when it is gated behind an exact-match check against your list.
Never combine credentials with a wildcard. If you truly need `Access-Control-Allow-Credentials: true`, you must name a single explicit origin · which the allow-list gives you for free.
Drop `null`. Do not put the literal `null` in your allow-list. If a legitimate flow seems to need it, fix the flow instead.
Scope the rest too · list only the methods and headers you actually use rather than allowing everything.
For how CORS fits into the wider set of habits that keep an API safe · auth, rate limiting, input validation · read our [API security best practices](/blog/api-security-best-practices) guide next.
Wrapping up
CORS misconfigurations are sneaky precisely because everything looks fine in your browser · the app works, the errors are gone, the demo ships. The danger only shows up when a request arrives that your own frontend would never send, and by then it is an attacker sending it. An explicit allow-list, no arbitrary reflection, no wildcard-with-credentials, and no `null` closes the hole for good.
If you built your app with an AI code generator, assume the CORS config was written to silence an error, not to defend your users · then go verify it. Run the free [CORS Tester](/tools/cors-tester) against your API, and run a full scan at simplyscan.io to catch the rest of the permissive defaults hiding in your backend.