CORS Security: The Complete Guide to Cross-Origin Resource Sharing
Understand CORS from a security perspective. Learn how misconfigurations lead to data theft, and implement secure cross-origin policies that protect your users and APIs.

Cross-Origin Resource Sharing (CORS) is one of the most misunderstood web security mechanisms. Implemented correctly, it enables secure cross-origin requests. Implemented incorrectly, it can expose your users' data to attackers. This guide explains CORS security from the ground up.
The Same-Origin Policy Foundation
Before understanding CORS, you must understand the Same-Origin Policy (SOP). This fundamental browser security mechanism prevents scripts on one origin from accessing data on another origin.
What Defines an Origin?
An origin consists of three components:
- Protocol: http vs https
- Host: example.com vs api.example.com
- Port: :80 vs :8080
If any component differs, the origins are different. https://example.com and https://api.example.com are different origins.
What SOP Blocks
- Reading responses from cross-origin fetch/XHR requests
- Accessing cross-origin iframe content
- Reading cross-origin canvas data
What SOP Allows
- Loading cross-origin images, scripts, stylesheets
- Submitting forms to cross-origin targets
- Embedding cross-origin media
How CORS Works
CORS relaxes the Same-Origin Policy in a controlled way. It allows servers to specify which origins can access their resources.
Simple Requests
For "simple" requests (GET, HEAD, POST with standard content types), the browser adds an Origin header:
GET /api/data HTTP/1.1
Origin: https://example.com
The server responds with CORS headers:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
If the origin matches, the browser allows JavaScript to access the response.
Preflight Requests
For non-simple requests (PUT, DELETE, custom headers, non-standard content types), the browser first sends an OPTIONS preflight:
OPTIONS /api/data HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
The server must explicitly allow the method and headers:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: PUT, POST, DELETE
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 86400
CORS Response Headers
Access-Control-Allow-Origin
Specifies which origins can access the resource:
*- Any origin (public APIs only)https://example.com- Specific origin- Dynamic value based on request Origin header
Access-Control-Allow-Credentials
When set to true, allows requests with cookies and authentication headers. Cannot be used with Access-Control-Allow-Origin: *
Access-Control-Allow-Methods
Lists allowed HTTP methods for preflight responses.
Access-Control-Allow-Headers
Lists allowed request headers for preflight responses.
Access-Control-Expose-Headers
Lists response headers that JavaScript can access. By default, only "simple" response headers are exposed.
Access-Control-Max-Age
How long preflight results can be cached (in seconds).
CORS Security Vulnerabilities
Vulnerability 1: Reflecting Origin Without Validation
The most dangerous misconfiguration:
// DANGEROUS: Don't do this!
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Credentials', 'true');
next();
});
This allows any website to make authenticated requests to your API and read the responses. An attacker can steal user data by getting victims to visit a malicious page.
Vulnerability 2: Weak Origin Validation
// DANGEROUS: Substring matching
const origin = req.headers.origin;
if (origin.includes('example.com')) {
res.header('Access-Control-Allow-Origin', origin);
}
An attacker can use attacker-example.com or example.com.attacker.com to bypass this check.
Vulnerability 3: Null Origin Acceptance
// DANGEROUS: Accepting null origin
if (origin === 'null' || allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
The null origin can be triggered by sandboxed iframes, local files, and redirects. Never trust it.
Vulnerability 4: Wildcard with Credentials
Browsers block this combination, but server misconfigurations can still cause issues:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true // Browser will reject!
Secure CORS Implementation
Use an Allowlist
const allowedOrigins = new Set([
'https://example.com',
'https://app.example.com',
'https://staging.example.com'
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.has(origin)) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Credentials', 'true');
}
next();
});
Validate Origin Properly
function isAllowedOrigin(origin) {
if (!origin) return false;
try {
const url = new URL(origin);
// Exact match or subdomain of allowed domain
return url.hostname === 'example.com' ||
url.hostname.endsWith('.example.com');
} catch {
return false;
}
}
Handle Preflight Correctly
app.options('*', (req, res) => {
const origin = req.headers.origin;
if (isAllowedOrigin(origin)) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Max-Age', '86400');
}
res.status(204).end();
});
CORS for Different Scenarios
Public API (No Authentication)
Access-Control-Allow-Origin: *
Safe for truly public data that doesn't require authentication.
Authenticated API (Single App)
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Authenticated API (Multiple Apps)
// Dynamic, validated origin
const origin = req.headers.origin;
if (allowedOrigins.has(origin)) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Vary', 'Origin'); // Important for caching!
}
Third-Party Widget Integration
// Allow specific partners
Access-Control-Allow-Origin: https://partner-site.com
Access-Control-Allow-Methods: GET
// No credentials for third-party access
The Vary Header: Often Forgotten
When dynamically setting Access-Control-Allow-Origin, always include:
Vary: Origin
Without this, CDNs and caches might serve responses with the wrong origin header, causing security issues or broken functionality.
Testing CORS Security
Manual Testing
// Test from browser console on attacker.com
fetch('https://api.target.com/user/profile', {
credentials: 'include'
})
.then(r => r.json())
.then(data => console.log('Stolen data:', data))
.catch(err => console.log('CORS blocked:', err));
Automated Testing with curl
# Test if arbitrary origin is reflected
curl -H "Origin: https://evil.com" -I https://api.target.com/data
# Look for:
# Access-Control-Allow-Origin: https://evil.com # BAD!
# Access-Control-Allow-Credentials: true # VERY BAD!
CORS Security Checklist
- Never reflect the Origin header without validation
- Use exact-match allowlists for origins
- Never accept the null origin
- Don't use wildcards with credentials
- Validate origin using URL parsing, not string matching
- Include Vary: Origin for dynamic CORS headers
- Minimize exposed headers and methods
- Use SecScanner to audit CORS configurations
- Test with malicious origins during security reviews
- Document your CORS policy for team awareness
CORS misconfigurations are a common source of data breaches. Take the time to implement it correctly, validate your configuration, and regularly test your endpoints.
Related Articles
Check Your Website Security
Want to see how your website measures up? Run a free security scan with SecScanner to identify vulnerabilities and get actionable remediation guidance.
Scan Your Website Free