A CORS misconfiguration is a set of response headers that lets an origin you do not control read responses from your application. In practice it rarely comes from the * wildcard. It comes from an allowlist that echoes back whatever Origin the caller sent, paired with Access-Control-Allow-Credentials: true.
The wildcard is the configuration everyone flags. It is also the one that cannot hand over an authenticated response, because the browser will not let it.
Key takeaways
Access-Control-Allow-Origin: *cannot expose a logged-in user's data. Browsers refuse to pair a wildcard with credentials, so it fails closed.- The exploitable pattern is reflection: read
Origin, echo it back, setAccess-Control-Allow-Credentials: true. That is a wildcard that works with cookies, which is the exact thing the rule exists to prevent. - Allowlists are usually string matching, and string matching on hostnames accepts
notyourdomain.comandyourdomain.com.attacker.net. Compare complete origins for equality. nullis not a trust level. It is the absence of one, and any attacker can produce it from a sandboxed iframe.- CORS is browser-enforced and governs reading, not sending. It is not access control, and it is not a CSRF defense.
What the same-origin policy actually blocks
An origin is a scheme, a host, and a port. Under the same-origin policy, a page can still send a request to a different origin. What it cannot do is read the answer. That distinction carries the entire subject. The request leaves the browser, your server authenticates it and runs it, and then the browser withholds the result from the script that asked.
CORS is the mechanism for relaxing that, one response at a time. Access-Control-Allow-Origin is the server naming which origin is permitted to read what it just returned. That is the whole job. It does not decide who may call the endpoint, and nothing outside a browser consults it.
Why the wildcard is the safe answer
Everyone knows Access-Control-Allow-Origin: * means any site can read the response. The half that gets missed is what happens when the request carries a session. Per MDN: "If a request includes a credential (most commonly a Cookie header) and the response includes an Access-Control-Allow-Origin: * header (that is, with the wildcard), the browser will block access to the response."
So the wildcard is not a way to leak the logged-in user's data. It is precisely the configuration that cannot. It exposes what an anonymous request would have received anyway. On a public endpoint, that is not a finding, it is the intended use. Where it does deserve a second look is an endpoint that is only reachable from inside a network, because there the anonymous response was never public to begin with.
The restriction runs through the rest of the preflight too. For credentialed requests, MDN notes the server "must not specify the * wildcard" for Access-Control-Allow-Headers or Access-Control-Allow-Methods either. Each has to be an explicit list. The rule is consistent: the moment credentials enter, wildcards stop being wildcards.
That consistency is exactly what produces the real bug.
The CORS misconfiguration that matters is reflection
A team needs the front end on one origin to call the API on another with the session cookie attached. They set Access-Control-Allow-Credentials: true, the wildcard stops working, and the console reports that the origin is not allowed. The fastest change that makes the error go away is to send back whichever origin asked.
Origin: https://attacker.example
Access-Control-Allow-Origin: https://attacker.example
Access-Control-Allow-Credentials: true
That is a wildcard that works with cookies. Every origin is now on the allowlist, including ones the victim did not choose to visit. The OWASP Web Security Testing Guide puts it plainly: "the server returns back the origin header without any additional checks, which can lead to access of sensitive data."
The attack needs no exotic setup. A logged-in victim loads any attacker-controlled page. Script there issues a fetch with credentials: "include", the browser attaches the cookie, your server authenticates the request normally and answers it, and the response passes the CORS check because the header names the attacker's own origin. The script reads it and forwards it. Nothing was bypassed. Every layer did its job with the configuration it was handed.
The allowlist is usually string matching
Reflection with a check in front of it is the common middle ground, and the check is usually a substring test. Hostnames are a bad thing to substring.
origin.endsWith("example.com")also acceptshttps://notexample.com, which an attacker can simply register.origin.startsWith("https://example.com")also acceptshttps://example.com.attacker.net.- An unanchored regular expression accepts both of the above, and one where the dot was never escaped,
/example.com/, will happily matchhttps://exampleXcom.attacker.net.
Allowing every subdomain has a subtler version of the same problem. It turns each one into a key to the API, so a forgotten subdomain pointed at a dangling DNS record, or one cross-site scripting flaw on a marketing page nobody considers sensitive, becomes read access to authenticated data. The allowlist is honored exactly as written. It was just written to trust more than anyone intended.
The fix is unglamorous. Keep a fixed list of complete origin strings, compare with equality, and emit the caller's origin only when it is in the list. If the list genuinely has to be dynamic, it should come from configuration or a datastore, never from parsing the value being tested.
null is not a trust level
Sooner or later something legitimate sends Origin: null, a developer opens a page from disk or hits a redirect, and adding null to the allowlist makes the error stop.
MDN lists when browsers send it: schemes outside http, https, ftp, ws and wss; documents "generated from a data: URL, or that do not have a creator browsing context"; "redirects across origins"; and "iframes with a sandbox attribute whose value doesn't include allow-same-origin."
That last case is the problem, because it is available to anyone. An attacker puts a sandboxed iframe on their own page, runs script inside it, and every request it makes carries Origin: null. OWASP is blunt about the consequence: "The null origin serves as a 'wildcard' that can be exploited."
null does not name a party. It means the browser could not attribute the request to an origin at all. Allowing it allows every request that could not be attributed, which is not a smaller set than allowing everyone.
The cache turns one leak into many
Reflection creates a second failure that needs no attacker at all. If a response varies by Origin and the response does not say so, a shared cache treats one representation as good for every caller. MDN's guidance is that when a server specifies a single origin rather than the wildcard, it "should also include Origin in the Vary response header."
Without it, a CDN or proxy can store a response carrying Access-Control-Allow-Origin: https://partner.example and later serve that response, headers and all, to somebody else. The allowlist was correct. The cache republished its answer to an origin that was never on it.
Two things CORS is not
It is not access control. The entire mechanism is a browser choosing to honor an instruction. Origin is a request header, so anything that is not a browser sends whatever value it likes or omits it. A script, a proxy, a mobile client, a command-line request: none of them ask permission to read a response they have already received. Access control is a server-side decision about who the caller is and what they are entitled to, and no CORS configuration becomes that.
It is not CSRF protection. A simple request, meaning GET, HEAD, or POST with a form or plain-text content type, is sent without a preflight and processed by the server normally. CORS withholds only the response. When the harm is done on arrival, a transfer, a deletion, a permission change, blocking the read protects nothing that mattered. OWASP's CSRF prevention guidance keeps token-based defenses as the primary control and treats Origin and Referer verification as a defense-in-depth layer on top, not a replacement.
Reading the headers
| Configuration | What a browser does with it | Where it belongs |
|---|---|---|
ACAO: *, no credentials | Any origin reads the anonymous response | Public, unauthenticated endpoints. Intended use. |
ACAO: * plus Allow-Credentials: true | The CORS check fails and the read is blocked | Nowhere. It does not function. |
Reflected Origin plus credentials | Every origin reads authenticated responses | Nowhere. This is the vulnerability. |
| Prefix or suffix match on the host, plus credentials | Lookalike domains pass the check | Nowhere. Registration is cheap. |
null in the allowlist, plus credentials | Any sandboxed frame reads the response | Nowhere. |
Equality against a fixed list, plus Vary: Origin | Only listed origins read the response | The pattern to aim for. |
Nothing here is difficult, and none of it is new. It persists because the finding that gets reported is the one that is easy to see, and the header that is easy to see is the harmless one.
The wildcard is the one CORS header that cannot hand over a logged-in user's data. Nearly every configuration written to replace it can.
Related readingPart of our series on modern application security testing. Start with What You Should Know About Application Security Testing, then read Authentication Is Not Authorization on the question a valid session still leaves unanswered.
Frequently asked questions
Is Access-Control-Allow-Origin: * a vulnerability?
On its own, usually not. Browsers refuse to expose a response to a credentialed request when the header is the wildcard, so it can only ever reveal the anonymous response, which is what any unauthenticated client would have received anyway. On a public endpoint that is the intended configuration. It becomes worth investigating when the endpoint is reachable only from inside a network, because there the anonymous response was never public.
What causes most CORS misconfigurations?
Origin reflection. A developer needs cross-origin requests to carry cookies, sets Access-Control-Allow-Credentials: true, finds the wildcard no longer works, and resolves it by echoing back whatever Origin the caller sent. That combination is a wildcard that also works with credentials, which is precisely what the rule restricts. OWASP describes it as returning the origin header "without any additional checks, which can lead to access of sensitive data."
Does CORS protect my API from unauthorized access?
No. CORS is enforced by browsers and governs whether a page may read a response it already received. Anything that is not a browser ignores it, and Origin is a request header any client can set to any value. Access control has to be a server-side decision about who the caller is and what they are entitled to. No CORS configuration becomes that.
Does CORS prevent CSRF?
No. A simple request, meaning GET, HEAD, or POST with a form or plain-text content type, is sent without a preflight and processed by the server normally. CORS only withholds the response from the calling script, so for a state change the damage is done before the read is blocked. OWASP keeps token-based CSRF defenses as the primary control and treats Origin and Referer verification as defense in depth on top.
Why is it unsafe to allow the null origin?
Because null is not a party, it is the absence of an attributable origin, and any attacker can produce it on demand. An iframe with a sandbox attribute that omits allow-same-origin sends Origin: null on every request it makes, as do documents from data: URLs and requests following a cross-origin redirect. Allowlisting it therefore allows every request the browser could not attribute, which is not a smaller set than allowing everyone.
Do I need Vary: Origin?
Yes, whenever the Access-Control-Allow-Origin value changes based on the request rather than being a fixed wildcard. Without it, a shared cache or CDN can store one caller's response along with its allow-origin header and serve that header to a different origin later. MDN's guidance is that a server specifying a single origin should also include Origin in the Vary response header.