CORS & Preflight Requests
Interview Question: "What is CORS, and why does it exist as a browser security mechanism? Explain what triggers an HTTP
OPTIONSpreflight request, how preflight caching works, and whyAccess-Control-Allow-Origin: *fails when credentials are included."
Cross-Origin Resource Sharing (CORS) is a browser-enforced security protocol (W3C / WHATWG standard) that relaxes the restrictive Same-Origin Policy (SOP).
It defines a standardized mechanism for a server to explicitly declare which foreign origins (domains, schemes, or ports) are authorized to read sensitive resources loaded in a client web browser.
Foundational Concept: What Defines an "Origin"?
Two URLs share the Same Origin if and only if their Scheme, Host, and Port are strictly identical:
Comparing with Reference: https://example.com:443/app/index.html
├── https://example.com/api/data -> SAME ORIGIN (Default HTTPS port 443 matches)
├── http://example.com/api/data -> CROSS ORIGIN (Scheme mismatch: http vs https)
├── https://api.example.com/data -> CROSS ORIGIN (Subdomain mismatch)
└── https://example.com:8443/data -> CROSS ORIGIN (Port mismatch: 8443 vs 443)
The #1 Misconception: The Same-Origin Policy does NOT block browsers from sending cross-origin requests. It blocks the browser's JavaScript from reading the response unless the server explicitly grants permission via CORS response headers!
The Two Request Classes: Simple vs. Preflighted
Browsers split cross-origin requests into two categories depending on their potential to mutate server state:
INCOMING BROWSER REQUEST
│
┌──────────────────────────┴──────────────────────────┐
▼ ▼
[ SIMPLE REQUEST ] [ PREFLIGHTED REQUEST ]
• Method: GET, HEAD, or POST • Method: PUT, DELETE, PATCH
• Content-Type: • Custom Headers: Authorization, X-Api-Key
- application/x-www-form-urlencoded • Content-Type: application/json
- multipart/form-data • Request paused; browser dispatches
- text/plain an HTTP OPTIONS preflight probe first!
• Dispatched immediately to server!
Why application/json Triggers a Preflight:
Historically, HTML <form> tags could only submit application/x-www-form-urlencoded, multipart/form-data, or text/plain.
Because modern REST and GraphQL APIs communicate via Content-Type: application/json or attach Authorization: Bearer <token> headers, virtually every modern API request requires a preflight probe!
The Preflight Request Handshake (HTTP OPTIONS)
Before transmitting a potentially destructive request (e.g., DELETE /users/42), the browser automatically sends an HTTP OPTIONS probe asking the server for permission:
Latency Optimization: Preflight Caching (Access-Control-Max-Age)
Sending two network roundtrips (OPTIONS followed by POST/DELETE) for every single API interaction would double API latency, adding 100–300 ms of overhead.
To eliminate redundant roundtrips, servers attach the Access-Control-Max-Age header:
Access-Control-Max-Age: 86400
This instructs the browser's internal CORS cache to cache the preflight approval for 86,400 seconds (24 hours). Subsequent cross-origin calls to matching routes execute in a single roundtrip!
The Lethal Trap: Wildcards vs. Credentials
A notorious bug in full-stack architecture is handling authenticated cross-origin requests (e.g., sending session cookies or Authorization headers with credentials: 'include').
// THE FATAL COMBINATION (REJECTED BY ALL BROWSERS):
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Why Browsers Block Wildcards with Credentials:
If a server returned * with credentials allowed, any malicious site on the internet could initiate a background fetch to api.bank.com, the browser would attach the user's live banking session cookies, and the malicious site could read their balance!
The Required Secure Pattern:
When Access-Control-Allow-Credentials: true is enabled, the backend must dynamically validate the incoming Origin header against a trusted whitelist and echo back the exact origin:
// CORRECT BACKEND CONFIGURATION:
Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Credentials: true
Vary: Origin
The
Vary: OriginHeader: Mandatory when dynamically reflecting origins. It instructs intermediate CDNs and caching proxies not to serve cached CORS responses intended for Origin A to Origin B.
Summary
"CORS is a browser security protocol that relaxes the Same-Origin Policy, enabling authorized cross-origin requests. Requests utilizing non-standard methods (PUT, DELETE) or custom headers (Authorization, application/json) trigger an automated HTTP OPTIONS preflight probe to verify permissions before the actual request is dispatched. Preflight overhead is mitigated via Access-Control-Max-Age caching. Crucially, when requests include credentials, using wildcard origins (*) is strictly forbidden by browsers; the server must validate and explicitly reflect the trusted origin."
Python Verification: CORS Preflight & Security Validation Server
The following executable Python script implements an HTTP server handling CORS preflight OPTIONS requests, validating origins, demonstrating preflight caching, and illustrating credential security constraints:
"""
CORS Preflight & Security Validation Server
Demonstrates:
1. Automated handling of HTTP OPTIONS preflight requests
2. Verification of Origin, Methods, and Allowed Headers
3. Preflight caching via Access-Control-Max-Age
4. Proper credential handling without illegal wildcard origins
"""
from typing import Dict, Tuple
class CORSService:
def __init__(self, allowed_origins: list):
self.allowed_origins = set(allowed_origins)
def handle_request(self, method: str, headers: Dict[str, str]) -> Tuple[int, Dict[str, str], str]:
origin = headers.get("Origin")
# 1. Reject unapproved origins
if origin not in self.allowed_origins:
return 403, {}, "CORS Policy Violation: Origin unauthorized"
# 2. Handle HTTP OPTIONS Preflight Probe
if method == "OPTIONS":
req_method = headers.get("Access-Control-Request-Method", "")
req_headers = headers.get("Access-Control-Request-Headers", "")
response_headers = {
"Access-Control-Allow-Origin": origin, # Explicit origin (NOT wildcard *)
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Authorization, Content-Type",
"Access-Control-Max-Age": "86400", # Cache preflight for 24 hours
"Vary": "Origin" # Prevent CDN cache poisoning
}
return 204, response_headers, ""
# 3. Handle Actual Request (e.g., DELETE /api/resource)
response_headers = {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
"Content-Type": "application/json"
}
return 200, response_headers, '{"status": "success", "message": "Resource mutated"}'
def main():
print("=== CORS Preflight & Security Verification ===\n")
service = CORSService(allowed_origins=["https://dashboard.example.com"])
# Test Case 1: Preflight OPTIONS Request from Authorized Origin
print("--- 1. Handling Preflight OPTIONS Request ---")
preflight_headers = {
"Origin": "https://dashboard.example.com",
"Access-Control-Request-Method": "DELETE",
"Access-Control-Request-Headers": "authorization, content-type"
}
status, res_headers, _ = service.handle_request("OPTIONS", preflight_headers)
print(f"Response Status: {status} (Expected 204 No Content)")
for k, v in res_headers.items():
print(f" {k}: {v}")
# Test Case 2: Actual DELETE Request following preflight
print("\n--- 2. Handling Actual DELETE Request ---")
actual_headers = {
"Origin": "https://dashboard.example.com",
"Authorization": "Bearer secret_token_123"
}
status, res_headers, body = service.handle_request("DELETE", actual_headers)
print(f"Response Status: {status} OK")
print(f"Body: {body}")
# Test Case 3: Malicious Origin Blocked
print("\n--- 3. Handling Request from Malicious Origin ---")
bad_headers = {"Origin": "https://evil-hacker.com"}
status, _, body = service.handle_request("POST", bad_headers)
print(f"Response Status: {status} Forbidden | {body}")
if __name__ == "__main__":
main()