HTTPS, TLS, and the headers that protect your visitors

Is your CORS policy letting any site read your responses?

You hit a CORS error at 11pm, you paste a header that makes it go away, you ship. Most of us have done exactly that. The trouble is that the header which clears the error fastest is also the one that opens your API to every website on the internet, so the fix sticks around in code that's otherwise careful.

So here's what to actually worry about. A script running on some site your logged-in user happens to visit makes a request to your API, carrying their cookies. Can it read what comes back? If your CORS headers are set the wrong way, the answer is yes, and "some site" means any site at all.

What the headers really say

The browser's same-origin policy normally stops a page on evil.example from reading a response from yourapi.com. CORS is how you poke deliberate holes in that wall for origins you trust. Two headers do the damage when they're set wrong together:

Access-Control-Allow-Origin says which origins may read your responses. Access-Control-Allow-Credentials: true says "and they may do it with cookies attached," meaning the request runs as a logged-in user. Each is fine alone. Combined carelessly, they hand authenticated data to anyone.

request
GET /api/me HTTP/2 Origin: https://evil.example Cookie: session=eyJ1aWQi...
response
HTTP/2 200
access-control-allow-origin: https://evil.example
access-control-allow-credentials: true
{ "email": "[email protected]", "apiKey": "live_8fh..." }
The server reflected the attacker's Origin and allowed credentials. A script on evil.example just read this logged-in user's account data.

Look at what happened in that response. The server took the Origin header off the incoming request, which the attacker controls, and echoed it straight back into Access-Control-Allow-Origin. That reflection is the trap. It reads like a dynamic allowlist, but what it really does is allow whoever asked, and paired with Allow-Credentials: true, it means a script on any site your logged-in user visits can call your API as them and read everything that comes back.

The three shapes of the bug

There are three patterns scanners flag. They're three versions of the same underlying mistake.

The wildcard: Access-Control-Allow-Origin: *. On its own the browser won't let it combine with credentials, which saves a lot of people. But if your endpoints are reachable without auth and return anything sensitive, * still exposes them to every origin.

The reflection: echoing the request's Origin back without checking it against a real list, as in the diagram. This is the dangerous one, because it sidesteps the wildcard-plus-credentials block. The origin isn't literally *, so the browser allows credentials, and you've effectively built a wildcard that does work with cookies.

The null origin: Access-Control-Allow-Origin: null. Sandboxed iframes and some redirects send Origin: null, and an attacker can produce that origin deliberately. Allowing null is allowing them.

Picture the consequence concretely. A logged-in user of yours opens a tab to an attacker's page, maybe from a link in an email. That page quietly fires a request to https://yourapi.com/api/me with credentials: 'include'. Your server reflects the origin, allows credentials, and the response, the user's email, tokens, account details, lands in the attacker's script. The user did nothing but open a tab.

How to check

You can probe this from a terminal by lying about your origin.

  1. curl -sI -H "Origin: https://evil.example" https://yourapi.com/api/me | grep -i access-control. If the response echoes back access-control-allow-origin: https://evil.example, your server is reflecting.
  2. Watch for access-control-allow-credentials: true in the same response. Reflection plus credentials is the critical combination.
  3. Try Origin: null too. If that comes back allowed, the null origin is in play.

Forging an Origin header is exactly the kind of thing SurfaceCheckr does for you. We send these probes against the endpoints we can reach without logging in, then report whether your server reflects an arbitrary origin, allows null, or pairs a wildcard with credentials. That's the honest boundary of it: we read the policy from the same vantage point an attacker has, and we can't sign in as one of your real users to see which authenticated fields actually spill. So we'll tell you the policy is reflecting. You'll know better than us how sensitive the data sitting behind it is.

The fix

Stop reflecting. Match the incoming origin against an explicit allowlist, and only then set the header. If an origin isn't on the list, send no CORS header at all and let the browser do its job.

// Express: reflects whatever asked
app.use((req, res, next) => {
res.set("Access-Control-Allow-Origin", req.headers.origin);
res.set("Access-Control-Allow-Credentials", "true");
next();
});
Never echo the request Origin back unchecked. An allowlist of exact origins is the only safe form when credentials are involved.

If an endpoint is genuinely public and carries nothing sensitive, * without credentials is fine and even correct. The danger is only ever credentials plus a permissive origin. And because this whole attack runs by riding your user's session, it's worth confirming your session cookies use SameSite, which limits whether that cookie gets attached cross-site in the first place.

Run the curl line with a fake Origin against your own API. If it echoes your lie back with credentials allowed, any site can read your users' data.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.