Security Headers and CORS
Security Headers and CORS
Whenever the browser renders a page, several protective mechanisms run in concert. Same-origin policy, CORS preflight, the script allow-list of CSP, HSTS, and the isolation headers introduced after Spectre. This article covers each header's role and common shapes, plus inspection tools.
1. Same-origin policy
The browser's default security model. Scripts from one origin (scheme://host:port) cannot freely read resources from another. CORS, CSP, and isolation headers are layered on top.
2. CORS
A mechanism for granting cross-origin resource access. RFC 6454 (Origin) and the WHATWG Fetch Standard.
The server must send headers like Access-Control-Allow-Origin for the browser to expose the response to JavaScript:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
Preflight (OPTIONS) — the browser sends an OPTIONS pre-request before the actual request when any of the following holds:
- The method is not simple (not GET, HEAD, or POST).
Content-Typeis not a simple value (application/x-www-form-urlencoded,multipart/form-data,text/plain);application/jsontriggers preflight.- Custom headers (other than
Authorization) are present.
The server must answer OPTIONS with appropriate Access-Control-* responses for the actual request to proceed.
Credentials — requests carrying cookies, HTTP authentication, or client certificates need credentials: 'include'. In that case the server's Access-Control-Allow-Origin cannot be * — an explicit origin is required, plus Access-Control-Allow-Credentials: true.
3. CSP
W3C CSP Level 2 (2016), Level 3 (in progress). Tells the browser which origins are allowed for scripts, styles, images, and connections on this page. The most effective tool for narrowing the blast radius of XSS.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
nonce, hash, strict-dynamic:
- nonce — embed an arbitrary base64 nonce in the inline script and declare the same value in CSP. A fresh value per response.
- hash — register the SHA-256 hash of the inline script in CSP.
- strict-dynamic — scripts trusted via nonce can dynamically load further trusted scripts. A modern pattern that reduces the limits of allow-list-style CSP.
script-src 'nonce-abc123' 'strict-dynamic';
Google's CSP Evaluator and CSP guide recommend the strict-dynamic model.
report-only — Content-Security-Policy-Report-Only reports violations without blocking. Use it as a regression-check spot when introducing new policy.
4. Other security headers
HSTS (HTTP Strict Transport Security) — RFC 6797 (2012). Forces HTTPS:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
max-age— duration in seconds. One year or longer recommended.includeSubDomains— extend to subdomains.preload— once on the HSTS preload list, browsers enforce it from the very first visit.
Preload registration is hard to reverse — apply with care.
X-Frame-Options or frame-ancestors — block iframe embedding to prevent clickjacking:
X-Frame-Options: DENY
# or CSP frame-ancestors
Content-Security-Policy: frame-ancestors 'none'
CSP's frame-ancestors is more expressive and is replacing X-Frame-Options.
X-Content-Type-Options — disables browser MIME sniffing. Prevents an application/json response from being interpreted as HTML and triggering XSS:
X-Content-Type-Options: nosniff
Referrer-Policy — controls how much information goes into the Referer header on outgoing requests:
Referrer-Policy: strict-origin-when-cross-origin
Options include no-referrer, same-origin, and strict-origin-when-cross-origin (modern default).
Permissions-Policy — formerly Feature-Policy. Controls browser features such as camera, microphone, and geolocation:
Permissions-Policy: camera=(), microphone=(), geolocation=(self)
X-XSS-Protection — a legacy header. Modern browsers ignore it (removed from Chrome 78). CSP replaces it.
5. Isolation headers (post-Spectre)
In 2018, Spectre and Meltdown exposed hardware side-channel attacks. Browsers introduced new headers for cross-origin isolation.
COOP (Cross-Origin-Opener-Policy):
Cross-Origin-Opener-Policy: same-origin
Blocks browsing-context sharing with cross-origin windows. Closes surfaces like window.opener.
COEP (Cross-Origin-Embedder-Policy):
Cross-Origin-Embedder-Policy: require-corp
Every resource the page loads must carry explicit permission (a CORP header or CORS).
CORP (Cross-Origin-Resource-Policy):
Cross-Origin-Resource-Policy: same-site
The resource itself declares from which origins it can be embedded. The resource-side opt-in for COEP environments.
When both COOP and COEP are satisfied, the page enters a crossOriginIsolated environment that unlocks powerful APIs such as SharedArrayBuffer and high-resolution timers. This may be excessive for general sites — apply only where the feature is needed.
6. Bundling via middleware
Most frameworks ship security-header middleware:
- Express —
helmet. - Fastify —
@fastify/helmet. - Next.js —
headers()innext.config.jsormiddleware.ts. - Spring Boot —
Spring Security'sheaders()DSL. - nginx, Caddy — server-side configuration.
helmet's defaults are a sensible starting point.
7. CORS narrow application
- The origin allow-list contains exact domains.
- Avoid wildcards unless the API is anonymous.
- credentials + wildcard is forbidden by the standard.
- Cache preflight (
Access-Control-Max-Age) to reduce OPTIONS cost.
8. Inspection tools
- securityheaders.com — score the response headers.
- csp-evaluator.withgoogle.com — analyze CSP policies.
- Mozilla Observatory — a comprehensive checkup.
- The browser DevTools
SecurityandNetworktabs.
Inspect periodically after rollout — new features can quietly disable a policy.
9. Common pitfalls
Missing OPTIONS handling — if the backend does not handle OPTIONS, all preflights fail. Handle it in router middleware.
Access-Control-Allow-Origin: * + credentials — violates the standard; the browser refuses.
unsafe-inline in CSP — XSS protection effectively vanishes. Use nonce, hash, or strict-dynamic.
HSTS preload's irreversibility — a misconfiguration leaves user browsers refusing HTTP for a long time.
X-Frame-Options vs CSP frame-ancestors conflict — when the two headers send different policies, the stricter one applies or behavior varies by browser. Keep policy consistent.
Loose dev headers leaking into production — wildcards used for testing flow into production. Separate per-environment configuration.
Nonce reuse — fresh per response is the point. A static nonce loses its protection.
Conflicts with CDN cache — caching responses that contain a nonce reuses the same nonce across users, widening the XSS surface. Authenticated pages should be uncached.
Closing thoughts
Security headers form a foundation that subsequent code naturally builds on once configured. Set CSP strict-dynamic + HSTS preload + nosniff + Referrer-Policy + frame-ancestors via helmet, Caddy, or next.config.js once, then check periodically with securityheaders.com. CORS should use an exact origin allow-list and proper preflight responses.
Next
- (end of security)
We refer to MDN CORS, MDN CSP, Google Web Fundamentals Strict CSP, RFC 6797 HSTS, Cross-Origin Isolation Guide, securityheaders.com, helmet official, and the OWASP Secure Headers Project.