What is a CORS Misconfiguration? How It Leaks Data

What is a CORS Misconfiguration? How It Leaks Data

Browsers block one site from reading another site’s responses by default. That rule is the same origin policy, and CORS is the controlled way to relax it. A CORS misconfiguration happens when a server relaxes that rule too far, so a malicious page can read responses meant only for the logged in user. The result is account data theft from inside the victim’s own browser session.

The same origin policy first

An origin is the triple of scheme, host, and port. https://app.acme.io:443 is one origin. http://app.acme.io is a different origin, and so is https://api.acme.io. The same origin policy lets a page send requests to another origin, but it stops the page’s JavaScript from reading the response unless that origin gives permission. So https://evil.example can fire a request at https://api.acme.io, but it cannot read what comes back. That read block is what protects your logged in data.

CORS, Cross Origin Resource Sharing, is the mechanism that grants the read permission on purpose. The server answers with headers that tell the browser which other origins are allowed to read the response.

What CORS relaxes and the headers involved

Two response headers carry most of the weight:

  • Access-Control-Allow-Origin names the origin that is allowed to read the response. It can be a single exact origin or the wildcard *.
  • Access-Control-Allow-Credentials, when set to true, tells the browser it is allowed to send cookies and read the response even though the request carried the user’s session.

That second header is the dangerous one. Without it, a cross origin request that includes cookies cannot be read by the calling page. With it, the calling origin can read authenticated responses. So the combination of a permissive Access-Control-Allow-Origin and Access-Control-Allow-Credentials: true is where account data leaks.

The browser is asking the server one question, may this other site read my logged in response, and a CORS misconfiguration answers yes to a site that should never hear yes.

The CORS misconfiguration patterns that leak data

Take an invented app, Acme Notes, with an API at https://api.acme-notes.io. Here are the bad patterns its team could ship.

Reflecting the Origin header back

The simplest mistake is to read the incoming Origin request header and echo it straight back into Access-Control-Allow-Origin. The server effectively trusts whatever origin asks. Watch what an attacker page at https://evil.example gets:

GET /api/account HTTP/1.1
Host: api.acme-notes.io
Origin: https://evil.example
Cookie: session=a1b2c3d4...

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

{"email":"sam@acme-notes.io","plan":"pro","apiKey":"sk_live_9f2..."}

The server reflected https://evil.example and allowed credentials. The victim’s cookie rode along, the server returned their account, and the attacker’s JavaScript can now read it. The email and API key are stolen.

Wildcard combined with credentials

You cannot legally pair Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. Browsers reject that pairing on a credentialed request. So teams that want both reach for reflection instead, which lands them back in the pattern above. The wildcard on its own is fine for truly public data, but the moment a route needs cookies, a wildcard cannot be the answer, and reflecting the origin is not a safe substitute.

Trusting the null origin

Some setups, like a sandboxed iframe or a request from a local file, send Origin: null. A server that allowlists the string null is trusting a value any attacker can produce from a sandboxed iframe:

GET /api/account HTTP/1.1
Host: api.acme-notes.io
Origin: null
Cookie: session=a1b2c3d4...

HTTP/1.1 200 OK
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

An attacker hosts a page that loads a sandboxed iframe, which sends Origin: null, and the server hands back the credentialed response. Never put null on a trust list.

Weak matching with endswith or startswith

Allowlist checks built on substring logic almost always leak. A check like origin.endswith("acme-notes.io") looks tight, but it accepts more than the team thinks:

# Intended allow: https://app.acme-notes.io
# endswith("acme-notes.io") also accepts:
https://evilacme-notes.io        # attacker registers this domain
https://acme-notes.io.evil.example  # attacker subdomain, also ends in the string? no,
                                    # but startswith and contains checks fail here too

The domain evilacme-notes.io ends with acme-notes.io, so the suffix check passes and the attacker controls that domain. A prefix check has the mirror flaw: startswith("https://acme-notes.io") accepts https://acme-notes.io.evil.example. A contains check is worse still. The fix is to compare against exact origin strings, not fragments.

Why a misconfiguration lets a site read your data

The attack does not need to steal a password. The victim is already logged in to Acme Notes, so their browser holds a valid session cookie. The victim then visits https://evil.example, perhaps from a link. That page runs JavaScript that calls https://api.acme-notes.io/api/account with credentials included. The browser attaches the Acme Notes cookie automatically because cookies are scoped to the destination, not the calling page. If the response carries a permissive Access-Control-Allow-Origin for evil.example plus Access-Control-Allow-Credentials: true, the browser lets the attacker’s script read the body. The script then ships the account data to a server the attacker controls. No phishing form, no malware, just one bad header pair.

How to detect a CORS misconfiguration

  • Send odd origins and read the response. Against an app you own, send requests with Origin: https://evil.example, Origin: null, and an origin that shares a suffix like https://evilacme-notes.io. If any of them comes back reflected in Access-Control-Allow-Origin alongside Access-Control-Allow-Credentials: true, you have a finding.
  • Audit the origin check in source. Search the codebase for where Access-Control-Allow-Origin is set. If the value comes from the request Origin header, or from endswith, startswith, or contains matching, that is the bug in source form.
  • Check every credentialed route. List the routes that return user data with cookies. Each one should allow only exact, known origins.

How to prevent a CORS misconfiguration

  • Keep a strict allowlist of exact origins. Hard code the full origins you trust, scheme and host and port, and compare with an exact string match. https://app.acme-notes.io either matches the list or it does not.
  • Never reflect an arbitrary Origin. If you echo the incoming origin, do it only after confirming it is on the allowlist, and send no CORS headers at all when it is not.
  • Do not combine the wildcard with credentials. For routes that need cookies, set one exact origin. Reserve Access-Control-Allow-Origin: * for genuinely public, non credentialed data.
  • Treat null as untrusted. Keep null off every allowlist. There is no safe reason to trust it for authenticated routes.
  • Scope cookies and use SameSite. Marking session cookies SameSite=Lax or Strict reduces what a cross origin call can carry, which limits the blast radius if a CORS rule slips.

This bug sits next to other ways a trust boundary gets crossed, so it pairs well with reading about CSRF and the wider access control category. Our web security glossary defines the origin and credential terms used here.

Why this rewards understanding the app

You do not find a CORS misconfiguration by replaying a fixed payload. You find it by understanding which origins the app should trust, which routes return logged in data, and how the server decides what to put in Access-Control-Allow-Origin. The bug is an assumption, that only the real frontend would ever ask, and the way to find it is to test that assumption with origins the app never planned for. That is the kind of bug an autonomous researcher built to test an app’s assumptions is made to surface. You can read more about that approach on our about page.

Frequently asked questions

What is a CORS misconfiguration?

It is a server setting that relaxes the browser’s same origin policy too far, so a site that should not be trusted can read responses meant for the logged in user. It usually comes from a permissive Access-Control-Allow-Origin value paired with Access-Control-Allow-Credentials: true. When that pairing is granted to an attacker controlled origin, the attacker’s JavaScript can read authenticated account data straight from the victim’s browser session.

Why is reflecting the Origin header dangerous?

Reflecting means the server reads the incoming Origin request header and echoes it back into Access-Control-Allow-Origin. That trusts whatever origin asks, including https://evil.example. Combined with Access-Control-Allow-Credentials: true, it lets any attacker page read the victim’s logged in response. Only reflect an origin after confirming it is on a strict allowlist, and send no CORS headers when it is not.

Can Access-Control-Allow-Origin be a wildcard with credentials?

No. Browsers reject Access-Control-Allow-Origin: * together with Access-Control-Allow-Credentials: true on a credentialed request. Teams that want both often switch to reflecting the origin instead, which reintroduces the leak. For routes that need cookies, set one exact origin. Reserve the wildcard for genuinely public data that carries no session.

How do you prevent a CORS misconfiguration?

Keep a strict allowlist of exact origins, comparing scheme, host, and port with an exact string match rather than endswith, startswith, or contains logic that lets evilacme.com slip through. Never reflect an arbitrary origin, never pair the wildcard with credentials, and keep null off every allowlist. Marking session cookies SameSite=Lax or Strict limits the damage if a rule slips.