CORS Architecture: Same-Origin Policy, Preflight Handshakes & Edge Server Runbook
A masterclass engineering manual detailing the W3C Fetch/CORS specification, the Same-Origin Policy boundary, simple vs preflight requests, the credentials wildcard trap, preflight cache optimization, and battle-tested production configs across Nginx, Express, Cloudflare Workers, Caddy, and Spring Boot.
1. The Same-Origin Policy (SOP) Security Boundary & The CORS Contract
# Emulate a cross-origin web browser request from an unauthorized client
curl -i -X OPTIONS https://api.example.com/v1/users \
-H "Origin: https://malicious-site.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization"A compliant, hardened server will omit the Access-Control-Allow-Origin header, triggering a browser-level rejection.
# Emulate an authorized client SPA making an authenticated request
curl -i https://api.example.com/v1/users \
-H "Origin: https://app.example.com" \
-H "Authorization: Bearer eyJhbGciOi..."A correctly configured server returns Access-Control-Allow-Origin matching the exact requesting origin.
| Origin Comparison URL | Outcome vs https://example.com:443 | Root Cause | Copy |
|---|---|---|---|
| https://example.com/api/v1 | Same Origin (Allowed) | Identical scheme, host, and port | |
| http://example.com/ | Cross-Origin (Blocked by SOP) | Scheme mismatch (http vs https) | |
| https://api.example.com/ | Cross-Origin (Blocked by SOP) | Host mismatch (subdomain difference) | |
| https://example.com:8443/ | Cross-Origin (Blocked by SOP) | Port mismatch (8443 vs default 443) |
2. Anatomy of the CORS Preflight (OPTIONS) Handshake & Timing Cascades
// This invocation triggers an automated browser preflight OPTIONS request
// Reason: method is PUT and Content-Type is application/json
const response = await fetch('https://api.example.com/v2/profile', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token_abc123'
},
body: JSON.stringify({ displayName: 'Alex' })
});The browser halts dispatch until the OPTIONS handshake yields matching Access-Control headers.
| Request Attribute | Simple Request (No Preflight) | Preflight Triggered (OPTIONS) | Copy |
|---|---|---|---|
| HTTP Verbs | GET, HEAD, POST | PUT, PATCH, DELETE, custom verbs | |
| Content-Type | text/plain, form-urlencoded, multipart | application/json, application/xml | |
| Request Headers | Accept, Accept-Language, Content-Language | Authorization, X-Custom-Header, traceparent | |
| ReadableStream | No upload stream attached | ReadableStream attached to request body |
3. The Credentials Trap: Cookies, Authorization Headers & Strict Origin Reflection
// Production-safe origin allowlist check with exact regex matching
const ALLOWED_ORIGINS = new Set([
'https://app.example.com',
'https://admin.example.com'
]);
export function getValidatedCorsOrigin(requestOrigin: string | undefined): string | null {
if (!requestOrigin) return null;
// Strict Set lookup avoids regex denial-of-service (ReDoS)
if (ALLOWED_ORIGINS.has(requestOrigin)) {
return requestOrigin;
}
return null; // Reject untrusted origins
}Only origins present in the explicit allowlist are mirrored in Access-Control-Allow-Origin.
| Configuration Pattern | Credential Support | Security & Spec Evaluation | Copy |
|---|---|---|---|
| Origin: * with Credentials: false | No Cookies / Auth | Standards Compliant (Public APIs) | |
| Origin: * with Credentials: true | Attempted Cookies | FATAL ERROR (Blocked by all browsers) | |
| Blind Origin Mirror (Origin: $http_origin) | Full Cookies Transmitted | CRITICAL VULNERABILITY (CSRF / Data Theft) | |
| Allowlist Validated Mirror + Credentials | Safe Authenticated SPA | RECOMMENDED PRODUCTION PATTERN |
4. Preflight Performance Optimization: Access-Control-Max-Age & Cache Topologies
# Verify that your reverse proxy returns an optimal preflight cache lifetime
curl -s -D - -o /dev/null -X OPTIONS https://api.example.com/v1/orders \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" | grep -i "access-control-max-age"Expected output: access-control-max-age: 86400 (or platform maximum).
| Browser Engine | Maximum Preflight Cache Duration | Specification Reference | Copy |
|---|---|---|---|
| Google Chrome / Chromium / Edge | 7,200 seconds (2 Hours) | Blink Network Cache Ceiling | |
| Mozilla Firefox (Gecko) | 86,400 seconds (24 Hours) | Gecko Network Engine Default | |
| Apple Safari (WebKit) | 3,600 seconds (1 Hour) | WebKit Security Cache Baseline | |
| Node.js / Undici / Server Clients | No Preflight Executed | Server-to-Server Bypasses SOP/CORS |
5. Hardened Production Server Configurations (Nginx, Express, Cloudflare & Caddy)
# /etc/nginx/conf.d/api.conf
map $http_origin $cors_origin {
default "";
"https://app.example.com" "$http_origin";
"https://admin.example.com" "$http_origin";
}
server {
listen 443 ssl http2;
server_name api.example.com;
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400 always;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain; charset=utf-8';
return 204;
}
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length, X-RateLimit-Remaining' always;
proxy_pass http://backend_upstream;
}
}Utilizes Nginx map module for zero-overhead origin allowlist matching.
import express from 'express';
import cors from 'cors';
const app = express();
const whitelist = new Set(['https://app.example.com', 'https://admin.example.com']);
const corsOptions: cors.CorsOptions = {
origin: (origin, callback) => {
// Allow non-browser requests (like mobile apps, server cURL) where origin is undefined
if (!origin || whitelist.has(origin)) {
callback(null, true);
} else {
callback(new Error('Cross-Origin Request Blocked by Security Policy'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
exposedHeaders: ['X-RateLimit-Remaining', 'X-RateLimit-Reset'],
credentials: true,
maxAge: 86400,
optionsSuccessStatus: 204
};
app.use(cors(corsOptions));Production Express CORS configuration with dynamic callback validation.
// Cloudflare Workers Edge CORS Gateway
const ALLOWED_ORIGINS = new Set(['https://app.example.com', 'https://admin.example.com']);
export default {
async fetch(request: Request, env: unknown, ctx: ExecutionContext): Promise<Response> {
const origin = request.headers.get('Origin');
const isAllowed = origin && ALLOWED_ORIGINS.has(origin);
const corsOrigin = isAllowed ? origin : 'null';
const corsHeaders = {
'Access-Control-Allow-Origin': corsOrigin,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '86400',
'Vary': 'Origin'
};
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders });
}
const response = await fetch(request);
const modifiedHeaders = new Headers(response.headers);
Object.entries(corsHeaders).forEach(([key, val]) => modifiedHeaders.set(key, val));
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: modifiedHeaders
});
}
};Edge Worker implementation ensuring Vary: Origin is preserved for CDN caches.
| Server Runtime | Preflight Status Code | Allowlist Mechanism | Copy |
|---|---|---|---|
| Nginx Reverse Proxy | 204 No Content | map $http_origin directive | |
| Express.js (cors package) | 204 No Content (optionsSuccessStatus) | origin: callback allowlist function | |
| Cloudflare Workers (Edge JS) | 204 No Content | Set.has(origin) validation | |
| Caddy v2 Web Server | 204 No Content | @cors_preflight named matcher |
Related Free Online Tools
Put these techniques into practice immediately with our privacy-first browser utilities:
Generate production-ready CORS headers for Nginx, Express, Apache, and Cloudflare Workers with preflight OPTIONS auditing and credential checks.
Audit OWASP security response headers, evaluate HSTS/CSP directives, and browse RFC 9110 HTTP status decision trees.
Convert terminal cURL CLI requests into modern JavaScript Fetch, Python Requests, and Axios calls with custom headers and body payloads.