Skip to content
pwnsy
web-securityintermediate#cors#web-security#api-security#same-origin-policy#appsec

CORS Misconfiguration: When Permissive Origins Leak Data

How CORS misconfigurations expose data: reflected origins with credentials, null and wildcard traps, and how to build a safe cross-origin policy.

Browsers stop one website from reading another website's data. That is the same-origin policy, and it is the reason a page on evil.example cannot quietly read your webmail. Cross-Origin Resource Sharing, CORS, is the mechanism that pokes controlled holes in that wall so legitimate cross-origin apps can work. A CORS misconfiguration is a hole poked too wide, and it lets the wrong site read responses it was never meant to see.

The mistakes are common because CORS is easy to get working and hard to get right. A config that makes the error messages go away in development often trusts far more than the developer realises.

What CORS actually decides

When a page on one origin makes a request to a different origin, the browser sends the request but withholds the response from the calling script unless the responding server opts in. The server opts in with response headers, chiefly Access-Control-Allow-Origin, which names the origin allowed to read the response. If the calling origin is not covered, the browser blocks the script from reading the body.

Two points matter and are easy to miss.

First, CORS governs reading the response, not sending the request. The request usually still reaches your server and can still have side effects. That is why CORS is not a substitute for CSRF defense. See CSRF Explained for that separate problem.

Second, credentials are a special case. For the browser to send cookies or auth headers cross-origin and let the script read the response, the server must set Access-Control-Allow-Credentials: true, and in that case Access-Control-Allow-Origin may not be the * wildcard. It must name a specific origin. This single rule is where most dangerous misconfigurations are born.

The reflected-origin trap

Applications often need to allow several origins, so a developer writes code that reads the incoming Origin request header and echoes it straight back in Access-Control-Allow-Origin, alongside Access-Control-Allow-Credentials: true. It passes every test, because any origin the developer tries is reflected and allowed.

It also allows every origin, including the attacker's. An attacker hosts a page that makes a credentialed request to your API. The browser attaches the victim's cookies, your server reflects the attacker's origin, sets credentials true, and the attacker's script reads the authenticated response. That is cross-origin theft of whatever the victim's session can see.

Request:   Origin: https://attacker.example
Response:  Access-Control-Allow-Origin: https://attacker.example
           Access-Control-Allow-Credentials: true

The server treated a value the attacker fully controls as an allowlist. It is the CORS equivalent of trusting whatever the client claims.

The other common mistakes

Reflection is the headline, but several variants cause the same outcome.

MisconfigurationWhy it is dangerous
Reflecting the Origin header with credentialsTrusts every origin, including the attacker's
Trusting the null origin with credentialsnull is reachable from sandboxed iframes and some redirects
Wildcard * on an endpoint that should be privateAny site can read the response (though not with credentials)
Substring or prefix matching on allowed domainsyoursite.example.attacker.com or attacker-yoursite.example slips through
Trusting all subdomains blindlyOne compromised or user-controlled subdomain becomes an entry point

The substring-matching case deserves a flag. Code that allows an origin because it "contains" your domain name, or "ends with" it, is easy to defeat with a lookalike domain the attacker registers. Origin matching has to be exact.

Reflection plus credentials is the whole vulnerability

If your server reads the Origin request header and copies it into Access-Control-Allow-Origin while also sending Access-Control-Allow-Credentials: true, you have effectively allowlisted the entire internet with the victim's cookies attached. This one pattern accounts for most exploitable CORS findings. Search your codebase for anywhere the Origin header is echoed back into a response, and treat every hit as suspect.

What CORS does not protect

It helps to be precise about scope, because CORS gets blamed for things it was never meant to stop.

CORS does not stop requests from being sent, so it is not CSRF protection. CORS does not authenticate anyone; it only decides which origin may read a response. And a permissive CORS policy on an endpoint that returns no sensitive data and requires no credentials is often harmless. The danger appears specifically when a loose policy meets authenticated, sensitive responses. This is why CORS issues frequently show up alongside broader API weaknesses. See API Abuse Explained and the access-control themes in the OWASP Top 10.

How to build a safe CORS policy

Treat the set of allowed origins as an allowlist you control, never as something derived from the request.

  1. Keep an explicit allowlist of exact origins. Hard-code, or configure, the small set of full origins that legitimately need cross-origin access, scheme and host and port included. Compare the incoming Origin against that list with exact string equality.
  2. Reflect only after a match. It is fine to set Access-Control-Allow-Origin to the request's origin, but only once you have confirmed it is in the allowlist. Never reflect an unvalidated origin.
  3. Send credentials only where they are needed. Set Access-Control-Allow-Credentials: true only on endpoints that require it, and only for allowlisted origins. Prefer designs that avoid credentialed cross-origin requests entirely.
  4. Never allowlist null. Reject the null origin outright. There is no safe reason to trust it with credentials.
  5. Avoid wildcards on anything sensitive. Reserve Access-Control-Allow-Origin: * for genuinely public, unauthenticated resources, and understand it cannot carry credentials anyway.
  6. Match origins exactly, not by substring. No contains, no startsWith, no endsWith. An attacker will register the domain that satisfies a loose check.
  7. Keep authorization behind the CORS layer. CORS decides who may read; your authorization decides who may access. Sensitive data should require proper authentication and authorization regardless of any CORS header.
  8. Set a tight Access-Control-Allow-Methods and headers list. Allow only the methods and request headers the endpoint actually uses, so the preflight does not advertise more than you intend.
Think allowlist, not echo

The safe mental model for CORS is the same as for every other trust decision on the server: decide in advance which origins you trust, and check each request against that fixed list. The moment your allowed origin is computed from a value the client sent, you have handed the decision to the attacker. Keep the list on the server and compare by exact match.

CORS is a deliberate relaxation of a browser rule that protects your users, so the burden is on you to relax it precisely. Keep a short allowlist of exact origins, reflect an origin only after it matches, restrict credentials to the endpoints that truly need them, and never trust null or a substring. For the broader picture of how these response headers fit together, see the HTTP Security Headers Reference, and cross-check your findings against real exploit patterns on our Exploit Intelligence dashboard.

The preflight, and why it is not a security boundary

To reason about CORS bugs you have to understand the preflight request, because developers often mistake it for a wall it is not. The browser sends an automatic OPTIONS request ahead of certain cross-origin calls, the ones that are not "simple" requests, to ask the server what it will permit. A request triggers a preflight when it uses a method beyond GET, HEAD, or POST, when it carries custom headers, or when it uses a content type outside a small allowed set. The server answers with Access-Control-Allow-Methods, Access-Control-Allow-Headers, and the origin decision, and only then does the browser send the real request.

The trap is believing the preflight protects the endpoint. It does not gate whether a request can reach your server in a harmful way. Two facts undercut that belief. First, simple requests skip the preflight entirely, so a cross-origin POST with an ordinary content type arrives with no OPTIONS check at all, which is one reason CORS gives no CSRF protection. Second, the preflight is enforced by the browser for the browser's own scripts. A non-browser client, a script on a server, or a tool like curl ignores CORS completely and will send whatever it wants. CORS is a rule browsers apply to protect their users from other websites, and it constrains nothing outside a browser.

The practical takeaway is that the preflight decides what a browser will let a foreign page read, and your server-side authorization decides what any client is allowed to do. Confusing the two leads people to lean on CORS for access control it was never built to provide.

A step-by-step exploitation walkthrough

Walking a single reflected-origin bug end to end shows exactly how a permissive header becomes stolen data. Nothing here needs an exploit in the classic sense. The server simply trusts a value it should not.

Suppose an API endpoint at https://api.example returns the logged-in user's profile, including tokens, to any authenticated caller. The server was written to support several front-end origins, so it reads the incoming Origin header and echoes it back, and it sets Access-Control-Allow-Credentials: true so that the browser sends and exposes the session cookie.

An attacker registers https://evil.example and hosts a page there. When a victim who is logged in to the application visits that page, a script on it calls the profile endpoint with credentials included. The victim's browser attaches the session cookie automatically, because cookies follow the destination, not the caller. The server sees Origin: https://evil.example, reflects it into Access-Control-Allow-Origin, and returns the profile with credentials allowed. The browser checks the response headers, sees that the attacker's origin is permitted with credentials, and hands the authenticated response body to the attacker's script. The script reads the tokens and sends them to a server the attacker controls.

The victim did nothing beyond visiting a web page while logged in elsewhere. No phishing form, no malware, no clicked download. The entire breach lived in one server-side decision to treat a client-supplied header as an allowlist. That is why the reflected-origin pattern is treated as the defining CORS vulnerability rather than a minor hygiene issue.

How to detect CORS misconfigurations

Finding these bugs is mechanical once you know the signals, and they show up in both the response headers and the code. The tests are cheap to run.

Signals in the responses:

  • Send a request with an Origin header set to a domain you control, for example https://probe.invalid, and see whether the response reflects that exact value in Access-Control-Allow-Origin. Reflection of an arbitrary origin is the primary red flag.
  • Check whether Access-Control-Allow-Credentials: true appears alongside a reflected or overly broad origin. That combination is what turns a permissive policy into credentialed data theft.
  • Send Origin: null and see whether the server responds with Access-Control-Allow-Origin: null. Trusting null is exploitable from sandboxed iframes.
  • Try near-miss origins to probe matching logic: a subdomain, a domain that contains your host as a substring, and a lookalike that ends with your domain name. A response that allows any of these reveals loose matching.

Signals in the code and configuration:

  • Any place the Origin request header is read and written into a response header without an exact-match check against a fixed list.
  • Allowlist logic that uses contains, startsWith, endsWith, or a regular expression that is not anchored, since these are the usual sources of bypass.
  • A wildcard Access-Control-Allow-Origin on an endpoint that returns anything user-specific or authenticated.
  • Framework or middleware CORS settings switched to a permissive "allow all origins" mode that was convenient in development and never tightened.
One probe finds most of these bugs

The fastest check is to replay an authenticated request with the Origin header changed to a domain you own, then read the response headers. If the server reflects your arbitrary origin and also sends Access-Control-Allow-Credentials: true, you have found the serious case. Automate this across your endpoints and treat every reflected arbitrary origin as a finding to be justified or fixed.

Common mistakes and misconceptions

A handful of beliefs about CORS cause the recurring bugs, and naming them plainly helps.

"CORS protects my API from attackers." It does not. CORS is a browser mechanism that decides which web origins may read a response. It does not stop non-browser clients, and it does not stop requests from being sent. A tool that ignores CORS reaches your endpoint exactly as before, so your authorization still has to stand on its own.

"A permissive CORS policy is always a vulnerability." Not necessarily. A wildcard origin on a genuinely public, unauthenticated resource that returns no sensitive data is often fine. The danger is specifically a loose policy on responses that carry authenticated or sensitive content. Judge the finding by what the endpoint returns.

"Reflecting the origin is fine because I validate it." Only if the validation is exact-match against a fixed list. Reflection after a substring or suffix check is not validation; it is a bypass waiting for the attacker to register the domain that satisfies it. The safe pattern reflects an origin solely after confirming it appears verbatim in the allowlist.

"The wildcard is dangerous, so I will reflect instead to be safe." This reasoning produces the worst outcome. The wildcard at least cannot carry credentials, which the browser forbids. Reflecting the origin so that credentials can flow reintroduces exactly the exposure the wildcard restriction was preventing, now for every origin including the attacker's.

"CORS replaces CSRF protection." It does not. Simple requests are sent with no preflight, and CORS never blocks a request from leaving the browser, so state-changing endpoints still need their own CSRF defenses regardless of any CORS header.

How CORS relates to the same-origin policy

It helps to hold the two ideas in the right relationship. The same-origin policy is the browser's default: script on one origin cannot read responses from another origin. That default is what keeps a random page from reading your webmail. CORS is the controlled exception to that default, a way for a server to say that a specific named origin is allowed to read specific responses. Everything about CORS security follows from this framing. You are widening a protective default, so the only safe widening is a narrow, deliberate one.

ConceptWhat it isWho enforces itDefault posture
Same-origin policyBrowser rule blocking cross-origin readsThe browserDeny by default
CORSServer-declared exceptions to that ruleBrowser, based on server headersWhatever the server permits
CSRF defenseProtection against unwanted state-changing requestsYour applicationSeparate and still required

Reading the table top to bottom, the safe posture is to keep the browser's deny-by-default in place for everything that does not have a concrete reason to be shared, and to open CORS only for the exact origins and endpoints that genuinely need it. When a CORS decision is derived from a client-supplied value, you have handed the browser's protective default back to the attacker.

Caching, the Vary header, and a subtle failure mode

Once you allow more than one origin by reflecting an allowlisted value, caching becomes a correctness and security concern that is easy to overlook. The problem is that the response now varies depending on the request's Origin header, and any cache in front of your server needs to know that.

Consider what goes wrong without care. Your server allows two legitimate origins and reflects whichever one matched. A shared cache, a content delivery network or a reverse proxy, stores the response to a request from the first origin, including its Access-Control-Allow-Origin value. A later request from the second origin gets served the cached response, which now names the wrong origin. In the benign direction this breaks the second site, since the browser rejects a response whose allowed origin does not match. In the dangerous direction, a cached response that names a trusted origin could be served to a context you did not intend, or a poisoned entry could pin an attacker-influenced value.

The fix is to add Vary: Origin to any response whose CORS headers depend on the request origin. That tells every cache to key its stored copy on the Origin header, so a request from one origin never receives a response computed for another. Any endpoint that reflects an allowlisted origin should send this header. It is a small addition that prevents a class of caching bugs that are hard to diagnose after the fact, because they only appear when a cache sits in the path and two origins share it.

This interacts with a broader principle already covered: keep the origin decision on the server and exact-match it. Reflection plus caching without Vary: Origin is one more way a value that seems safely allowlisted ends up applied to the wrong request. When you reflect, reflect only after an exact match, and mark the response as varying on the origin so no cache blurs the distinction.

Frequently asked questions

Does CORS protect my API from attackers?

No. CORS is a browser feature that controls which web origins may read a cross-origin response. It does not stop non-browser clients, which ignore it entirely, and it does not prevent requests from being sent. Your server-side authentication and authorization remain the real access control.

What is the single most dangerous CORS mistake?

Reflecting the request's Origin header back into Access-Control-Allow-Origin while also sending Access-Control-Allow-Credentials: true. That combination trusts every origin, including an attacker's, with the victim's cookies attached, which lets a malicious page read authenticated responses.

Why can I not use a wildcard origin with credentials?

The browser forbids it by design. When Access-Control-Allow-Credentials is true, Access-Control-Allow-Origin must name a specific origin and cannot be *. The rule exists to stop any site on the internet from reading credentialed responses, and unsafe reflection is often built to work around it, which reintroduces the very risk the rule prevents.

Is the null origin safe to allow?

No. The null origin is reachable from sandboxed iframes, some redirects, and documents loaded from local files, so an attacker can cause a request to carry it. Never place null in an allowlist, especially with credentials.

Does CORS replace CSRF protection?

No. CORS does not block requests from being sent, and simple requests skip the preflight entirely. State-changing endpoints still need dedicated CSRF defenses such as tokens or same-site cookies, independent of any CORS configuration.

How should I validate the Origin header?

Compare it against a fixed allowlist of exact origins using string equality, including scheme, host, and port. Reflect an origin into the response only after that exact match succeeds. Avoid contains, startsWith, endsWith, and unanchored regular expressions, since each is a known bypass.

Is a permissive CORS policy always a bug?

No. A wildcard origin on a genuinely public, unauthenticated resource that returns no sensitive data can be acceptable. The serious findings are loose policies on endpoints that return authenticated or sensitive responses, where a foreign origin gaining read access means data theft.

Sources & further reading

Sharetwitterlinkedin

Related guides