HTTP Security Headers Complete Reference - CSP, HSTS, COOP, COEP, Permissions Policy
First Published:
Last Updated:
This article is the single-page reference I wish I had when I started shipping production sites with strict cross-origin isolation, Content Security Policy nonces, and modern Permissions Policy syntax. It covers every HTTP security header in use today on the open web, each with the same five-part treatment: the threat model the header addresses, the full syntax including every directive, three recommended values calibrated for modern-strict, modern-permissive, and legacy-compatibility postures, the browser compatibility you can actually rely on in 2026, and the common mistakes I see in audits. After the per-header reference there are two deeper sections: a CSP deep dive that walks through nonces, hashes,
strict-dynamic, and the directive-level decisions that distinguish a CSP that protects from one that exists only to placate scanners, followed by three end-to-end reference configurations for single-page apps, server-rendered apps, and pure static sites.Whenever you want to validate the values from this reference against a real response, the companion HTTP Security Header Analyzer Tool on this site accepts a paste of
curl -I output (or DevTools Network tab response headers) and returns a per-header grade with the same recommendations you will find below. The tool runs entirely in your browser; nothing is sent to a server. Use it as a checkpoint after each configuration change.The table below maps every common web-layer threat covered by this article to its primary defensive header and lists the secondary headers that meaningfully reinforce the same protection. Each pairing is unpacked in the per-header sections that follow.
| Threat | Primary Header | Secondary / Defense-in-Depth |
|---|---|---|
| Cross-Site Scripting (XSS) | Content-Security-Policy (script-src, nonces, strict-dynamic) | X-Content-Type-Options: nosniff |
| Clickjacking | CSP frame-ancestors / X-Frame-Options | — |
| MITM / SSL Stripping | Strict-Transport-Security (HSTS, with preload) | — |
| CSRF Assist | Set-Cookie SameSite, Secure, HttpOnly, Partitioned | CSP frame-ancestors |
| Cross-Origin Info Leak (Spectre) | COOP + COEP + CORP (Cross-Origin Isolation) | — |
| MIME Sniffing | X-Content-Type-Options: nosniff | — |
| Referrer Leak | Referrer-Policy | — |
| Feature Abuse (camera, geolocation, USB…) | Permissions-Policy | — |
| Sensitive Cache Disclosure | Cache-Control: no-store | Set-Cookie (HttpOnly) |
Table of Contents
- 1. Why HTTP Security Headers Matter
- 2. How to Capture and Inspect Response Headers
- 3. The Modern Security Headers Reference
- 3.1 Strict-Transport-Security (HSTS)
- 3.2 Content-Security-Policy (CSP)
- 3.3 Content-Security-Policy-Report-Only
- 3.4 Cross-Origin-Opener-Policy (COOP)
- 3.5 Cross-Origin-Embedder-Policy (COEP)
- 3.6 Cross-Origin-Resource-Policy (CORP)
- 3.7 Permissions-Policy
- 3.8 Referrer-Policy
- 3.9 X-Frame-Options
- 3.10 X-Content-Type-Options
- 3.11 X-XSS-Protection (Deprecated)
- 3.12 Cache-Control (Security View)
- 3.13 Set-Cookie (Secure, HttpOnly, SameSite, Partitioned)
- 3.14 Clear-Site-Data
- 3.15 Server-Timing (Information Leakage View)
- 3.16 Origin-Agent-Cluster
- 3.17 Reporting-Endpoints and Report-To
- 3.18 Expect-CT (Deprecated)
- 3.19 Public-Key-Pins (Deprecated)
- 3.20 Other Headers Worth a Mention
- 4. CSP Deep Dive - Directives, Nonces, Hashes, and Strict Dynamic
- 5. Reference Configurations for SPA, SSR, and Static Sites
- 6. Tool: HTTP Security Header Analyzer
- 7. Frequently Asked Questions
- 8. Summary
- 9. References
1. Why HTTP Security Headers Matter
Security headers are unusual among web defenses in that they are declarative, server-emitted, and enforced by the user's own browser. The server says "do not allow inline scripts," "treat this connection as TLS-only for the next year," or "this resource is for same-origin consumers only," and modern browsers obey unconditionally. The cost to ship a header is essentially zero — a few extra bytes per response — and the value, when the header is correctly configured, is the elimination of entire vulnerability classes at the user agent rather than inside application code.The flip side is that headers are also unusually unforgiving. A
Content-Security-Policy that omits object-src 'none' leaves room for a Flash or PDF-based bypass. A Strict-Transport-Security that omits includeSubDomains protects only the apex hostname while leaving every subdomain stripable. A Permissions-Policy that uses the old Feature-Policy syntax silently does nothing in browsers that have moved on. The headers space rewards precision, and the goal of this article is to give you exactly that precision rather than the loose "set these five and you're fine" advice that dominates older blog posts.A practical motivator: in 2026 the bar for "secure by default" has moved. The Open Worldwide Application Security Project (OWASP) Secure Headers Project recommends a baseline of HSTS, CSP, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and the cross-origin trio (COOP, COEP, CORP) on any production HTTPS site. Major site scanners — including securityheaders.com, Mozilla Observatory, and the analyzer paired with this article — penalize sites that ship without them. Search engines do not yet treat header hygiene as a direct ranking factor, but Chrome DevTools surfaces missing headers in the Issues panel, and security review at large enterprises increasingly gates third-party integrations on whether the partner site clears a header audit. Headers are no longer optional polish; they are table stakes.
The remainder of this article is organized so you can read it linearly the first time, then return for spot lookups. Each header section follows the same five-part structure, and the headers are ordered roughly by impact and frequency-of-use, starting with the headers every site should have and ending with the deprecated headers that you should explicitly avoid.
2. How to Capture and Inspect Response Headers
Before changing a single header, get a clean baseline. The reliable cross-platform way iscurl -I, which issues a HEAD request and prints the response status line followed by all response headers. The -s flag suppresses the progress bar, and -L follows redirects so you see the final response rather than a 301:curl -sI -L https://example.com/
For a site behind an aggressive bot wall, you may need
curl -A "Mozilla/5.0 ..." to spoof a browser user-agent or -H "Accept-Language: en-US,en;q=0.9" to match what a real browser would send. Headers can change based on Accept and Accept-Encoding negotiation, so if a header you expect is missing, test with a real browser's exact request headers before concluding the server is misconfigured.The browser equivalent is the DevTools Network tab. Open any page, click the document request (the first row, usually a
200 OK of type document), and the Response Headers section in the right pane will list everything the server sent. Both Firefox and Chromium-based browsers copy headers as plain text via right-click, which is the format the HTTP Security Header Analyzer Tool on this site is designed to parse. The parser is tolerant of HTTP/2 pseudo-headers, the status line itself, and the RFC 7230 line-folding rules, so you can paste the raw block without trimming.A second technique that occasionally surfaces problems
curl misses is the browser's own report-only telemetry. If you ship Content-Security-Policy-Report-Only with a Reporting-Endpoints collection URL, the browser will POST violation reports to that URL whenever a real user's navigation would have been blocked by the policy. That signal is invaluable when rolling out CSP, and it is the only way to discover violations triggered by browser extensions or by region-specific third-party scripts that never appear on your development machine.Finally, remember that headers can vary by route, method, content type, and even by CDN cache state. Audit at least three URLs per site: the homepage, an authenticated page, and a static asset such as
/favicon.ico or /robots.txt. If a header is set at the application layer but not at the CDN edge, asset URLs will silently miss it.3. The Modern Security Headers Reference
3.1 Strict-Transport-Security (HSTS)
Purpose / Threat Model. HSTS instructs the browser to treat the host as HTTPS-only for a declared period of time. After the first successful HTTPS response carrying the header, the browser will internally rewrite any subsequenthttp:// URL for that host to https:// before issuing the request, defeating SSL-stripping attacks on the local network and any active man-in-the-middle that depends on intercepting the initial HTTP redirect. HSTS is the single highest-impact transport-layer header and should be present on every HTTPS site.Syntax. The header takes one mandatory directive (
max-age) and two optional flags (includeSubDomains, preload):Strict-Transport-Security: max-age=<seconds>; includeSubDomains; preload
max-age is the time, in seconds, that the browser should remember the policy. includeSubDomains extends the policy to every subdomain — api.example.com, assets.example.com, and so on — without their needing to emit the header themselves. preload is a non-standard signal that the site owner consents to inclusion in the preload list maintained by Google and shipped with Chrome, Firefox, Safari, and Edge; once preloaded, the browser knows the host is HTTPS-only even before the first request.Recommended Value.
| Posture | Value |
|---|---|
| Modern Strict | max-age=63072000; includeSubDomains; preload |
| Modern Permissive | max-age=31536000; includeSubDomains |
| Legacy / Migration | max-age=300 (5 minutes, for safe rollback) |
One year (
31536000 seconds) is the minimum max-age accepted by the HSTS Preload List submission form, and two years (63072000 seconds) is the value Google recommends for preload submission and that browser scanners flag as "long enough to count." If you are not yet sure you can serve every subdomain over HTTPS, start with a five-minute max-age and no includeSubDomains for a few days while you monitor for breakage, then ratchet up.Compatibility. Supported in every browser that anyone still tests against, including Internet Explorer 11. There is no compatibility risk to enabling HSTS.
Common Mistakes. Three patterns recur. First, shipping HSTS on a host that still has subdomains served over plain HTTP — once
includeSubDomains is committed, every subdomain must serve HTTPS forever, and the only way to recover is to wait out the max-age on every affected client. Second, including preload in the header before the host is actually submitted to and accepted into the preload list at hstspreload.org; the directive does nothing on its own. Third, applying HSTS at the application layer only, so that the CDN's edge response for cached static assets is missing the header and a user who visits the site for the first time via a static-asset URL never receives the policy.Official reference: MDN: Strict-Transport-Security, RFC 6797.
3.2 Content-Security-Policy (CSP)
Purpose / Threat Model. CSP is the single most effective defense against cross-site scripting (XSS) available to a web application. It lets the server declare exactly which origins are allowed to load scripts, styles, fonts, images, iframes, and connections, and which inline content is permitted. A correctly configured CSP renders the vast majority of XSS payloads inert because the browser refuses to execute them regardless of whether they appear in the rendered HTML. CSP also blocks form submissions to unexpected destinations, restricts which origins can embed your page (viaframe-ancestors, replacing X-Frame-Options), and can require Trusted Types for DOM sinks.Syntax. CSP is a directive list, separated by semicolons, in the form:
Content-Security-Policy: <directive-1> <source-list-1>; <directive-2> <source-list-2>; ...
The most commonly used directives:
| Directive | Controls |
|---|---|
default-src | Fallback for any fetch directive not explicitly set |
script-src | Where scripts may be loaded from |
script-src-elem | Where <script> elements may load from |
script-src-attr | Inline event handlers (almost always 'none') |
style-src | Where stylesheets may be loaded from |
style-src-elem | Where <style> and <link rel="stylesheet"> may load from |
style-src-attr | Inline style="..." attributes |
img-src | Image sources |
font-src | Font sources |
connect-src | XHR / fetch / WebSocket / EventSource targets |
frame-src | Where iframes may load from |
frame-ancestors | Who may embed this page (replaces X-Frame-Options) |
form-action | Where <form action="..."> may post |
base-uri | Allowed values for <base href="..."> |
object-src | Plugins (Flash, PDF embeds); set to 'none' |
report-to | Reporting group (paired with Reporting-Endpoints) |
report-uri | Legacy reporting URL (deprecated but still widely used) |
upgrade-insecure-requests | Auto-upgrade http: subresources to https: |
require-trusted-types-for | Force Trusted Types on script sinks |
trusted-types | Allowed Trusted Type policy names |
Source list keywords:
'self' matches the page's own origin; 'none' matches nothing; 'unsafe-inline' allows inline <script> or <style>; 'unsafe-eval' allows eval() or new Function(); 'nonce-<base64>' allows scripts with a matching nonce="..." attribute; 'sha256-<hash>' allows scripts whose body has that exact hash; 'strict-dynamic' lets a nonced or hashed script transitively load further scripts. Scheme-only sources such as https:, data:, and blob: are also valid.Recommended Value.
| Posture | Value |
|---|---|
| Modern Strict (nonce-based) | default-src 'self'; script-src 'nonce-{random}' 'strict-dynamic'; style-src 'self' 'nonce-{random}'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; object-src 'none'; require-trusted-types-for 'script'; upgrade-insecure-requests |
| Modern Permissive (allowlist) | default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none' |
| Legacy / Migration | default-src 'self' 'unsafe-inline' 'unsafe-eval' https: data: blob:; frame-ancestors 'self'; object-src 'none' |
Strict CSPs that use nonces and
'strict-dynamic' are the modern recommendation because they protect even against XSS payloads that try to whitelist themselves through allowlisted CDN URLs. They do require server-side rendering to inject a fresh nonce on every response, which is usually the migration cost worth paying.Compatibility. CSP Level 2 — the directives in the table above except
'strict-dynamic', report-to, and Trusted Types — is supported everywhere. CSP Level 3 features ('strict-dynamic', report-to, worker-src) are supported in Chromium and Firefox; Safari has partial support. Trusted Types ship in Chromium and (since Firefox 135) Firefox, with Safari support still pending; the directive is ignored in browsers that do not implement it, so it is safe to include.Common Mistakes. The single most common mistake is leaving
'unsafe-inline' in script-src because removing it broke something during development. The second is using default-src 'self' without also setting frame-ancestors, base-uri, and form-action — those three are not covered by the default-src fallback. The third is failing to include object-src 'none', which leaves a small attack surface for Flash and PDF-embed-based bypasses. The fourth, specific to CSPs with allowlists, is whitelisting an entire CDN such as https://cdn.example.com that itself hosts content uploaded by third parties — the allowlist then becomes the bypass.Official reference: MDN: Content-Security-Policy, W3C CSP Level 3.
3.3 Content-Security-Policy-Report-Only
Purpose / Threat Model. The report-only variant of CSP is identical in syntax but causes the browser to report violations rather than block them. It is the safe way to ship a new policy: deploy it as report-only first, collect a week or two of real-user reports, and only then promote it to the enforcing header. Both headers can be sent simultaneously, which is how production sites typically prototype the next iteration of a policy while still enforcing the current one.Syntax. Identical to Content-Security-Policy. Must include either
report-to or report-uri (or both, for compatibility) for the header to do anything useful.Recommended Value. Whatever policy you intend to migrate to next. There is no separate posture column for this header — it always carries the policy under evaluation.
Compatibility. Universal, with the same caveats as the enforcing CSP header.
Common Mistakes. Shipping report-only without configuring a reporting endpoint, which makes the header inert. Forgetting to keep the report-only header in place after the enforcing header is deployed — both should run in parallel during major policy revisions.
Official reference: MDN: Content-Security-Policy-Report-Only.
3.4 Cross-Origin-Opener-Policy (COOP)
Purpose / Threat Model. COOP isolates a browsing context (window or tab) from cross-origin documents. Withsame-origin, when a user navigates from your page to another origin, the new origin cannot access your window object through the opener reference, and a popup you open cannot reach back into your page. This prevents the entire class of attacks that abuse cross-origin window references — Spectre-style side-channel reads, cross-origin postMessage replay, tabnabbing variants — and is also one of the two preconditions for crossOriginIsolated, which unlocks SharedArrayBuffer and high-resolution timers.Syntax.
Cross-Origin-Opener-Policy: <directive>
| Directive | Behavior |
|---|---|
same-origin | Full isolation; cross-origin documents cannot share a browsing context group |
same-origin-allow-popups | Isolation, but popups your page opens are not isolated from you |
unsafe-none | Default; no isolation |
Recommended Value.
| Posture | Value |
|---|---|
| Modern Strict | same-origin |
| Modern Permissive | same-origin-allow-popups |
| Legacy | unsafe-none (the default; do not send the header) |
same-origin-allow-popups is the right choice if your site uses OAuth popups, payment-provider checkout windows, or SSO redirects via window.open, because same-origin would prevent those popups from communicating back to your page via postMessage.Compatibility. Supported in Chromium 83+, Firefox 79+, and Safari 15.2+. Older browsers ignore the header without error.
Common Mistakes. Shipping
same-origin on a site that depends on OAuth or payment popups and discovering at the worst possible moment that the popup callback can no longer talk to the opener. The same-origin-allow-popups value exists for exactly this case. The second common mistake is treating COOP as a replacement for X-Frame-Options — it is not; COOP governs window-level relationships, while X-Frame-Options (or CSP frame-ancestors) governs iframe embedding.Official reference: MDN: Cross-Origin-Opener-Policy.
3.5 Cross-Origin-Embedder-Policy (COEP)
Purpose / Threat Model. COEP, combined with COOPsame-origin, puts the page into the crossOriginIsolated state. In that state, every cross-origin subresource — image, script, font, stylesheet, fetch response — must either be served with Cross-Origin-Resource-Policy: cross-origin or use CORS. The practical effect is that the page can use SharedArrayBuffer, performance.measureUserAgentSpecificMemory(), and high-resolution timers that are otherwise gated behind cross-origin-isolation as a Spectre mitigation. COEP is essentially a precondition: enable it only when you actually need cross-origin isolation, because it can break cross-origin subresource loading.Syntax.
Cross-Origin-Embedder-Policy: <directive>
| Directive | Behavior |
|---|---|
require-corp | Every cross-origin subresource must opt in via CORP or CORS |
credentialless | Cross-origin no-credentials requests are allowed without CORP |
unsafe-none | Default; no requirement |
Recommended Value.
| Posture | Value |
|---|---|
| Modern Strict (needs cross-origin isolation) | require-corp |
| Modern Permissive (needs isolation, has third-party subresources) | credentialless |
| Legacy / does not need isolation | unsafe-none (do not send) |
credentialless was added in Chromium 96 specifically to ease COEP adoption for sites that embed third-party content from origins that will not send CORP. The browser strips credentials from such requests and treats them as opt-out from CORP.Compatibility. Chromium 83+, Firefox 79+, Safari 15.2+.
credentialless is Chromium 96+ and Firefox 110+ only; Safari does not yet support it.Common Mistakes. Enabling COEP without first auditing every cross-origin subresource — fonts, images on a different CDN, embedded YouTube players, social sharing widgets — and discovering on production that several of them now refuse to load. Shipping COEP without also shipping COOP
same-origin; cross-origin isolation requires both.Official reference: MDN: Cross-Origin-Embedder-Policy.
3.6 Cross-Origin-Resource-Policy (CORP)
Purpose / Threat Model. CORP is the per-resource opt-in for COEP. A response carryingCross-Origin-Resource-Policy: cross-origin declares that it is safe to be loaded by any origin. same-origin declares it is only for same-origin consumers, and same-site allows registrable-domain consumers (a.example.com can load b.example.com, but evil.com cannot). CORP is independent of COEP — even sites that do not need cross-origin isolation should set CORP on private resources to defeat Spectre-style cross-origin reads.Syntax.
Cross-Origin-Resource-Policy: <directive>
| Directive | Behavior |
|---|---|
same-origin | Only same-origin consumers may load this resource |
same-site | Same-site (registrable domain) consumers may load it |
cross-origin | Any origin may load it |
Recommended Value.
| Posture | Value |
|---|---|
| Modern Strict (private resources) | same-origin |
| Modern Strict (public assets) | cross-origin |
| Modern Permissive | same-site |
| Legacy | (header omitted) |
A common pattern is
same-origin on the HTML document and any JSON API responses, and cross-origin on CDN-hosted static assets that other sites are explicitly allowed to embed (open-source documentation images, public-API JS SDKs).Compatibility. Chromium 73+, Firefox 74+, Safari 12+. Older browsers ignore it harmlessly.
Common Mistakes. Setting
same-origin on a CDN that serves assets meant for embedding on customer sites, breaking those embeds without anyone noticing until customer-facing pages 404 on the asset. Omitting CORP from API responses; even if the API uses cookie auth and is otherwise protected, CORP closes a Spectre side-channel that CORS preflight does not.Official reference: MDN: Cross-Origin-Resource-Policy.
3.7 Permissions-Policy
Purpose / Threat Model. Permissions-Policy controls which browser features and APIs are allowed in the current document and in any iframes it embeds. It is the successor to Feature-Policy, with which it shares purpose but not syntax. It lets you say "this page may use the camera but no embedded iframe may," or "geolocation is disabled site-wide," or "this page does not need any payment, USB, or serial APIs." Beyond defense in depth — a compromised script cannot escalate to an API the page never asked for — the header also serves as a privacy guarantee to users and as compliance evidence for accessibility and data-protection audits.Syntax.
Permissions-Policy: feature1=(allowlist1), feature2=(allowlist2), ...
Each feature is followed by a parenthesized allowlist.
() means "disable for everyone," (self) means "this origin only," (self "https://trusted.example.com") allows the current origin plus an explicit cross-origin, and * means "allow any origin." Note that the values inside the parentheses are space-separated, with cross-origin URLs wrapped in double quotes — this is a meaningful syntax change from Feature-Policy, which used semicolons and unquoted URLs.Commonly restricted features include
accelerometer, ambient-light-sensor, autoplay, battery, camera, clipboard-read, clipboard-write, cross-origin-isolated, display-capture, encrypted-media, fullscreen, geolocation, gyroscope, hid, idle-detection, keyboard-map, magnetometer, microphone, midi, payment, picture-in-picture, publickey-credentials-get, screen-wake-lock, serial, sync-xhr, usb, web-share, xr-spatial-tracking.Recommended Value.
| Posture | Value |
|---|---|
| Modern Strict (no powerful APIs needed) | accelerometer=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), serial=(), usb=() (extend with every other feature your site does not use) |
| Modern Permissive | camera=(self), microphone=(self), geolocation=(self), payment=(self), fullscreen=(self), display-capture=() |
| Legacy | (header omitted) |
The strict value is a long denylist of every powerful feature your site does not use; tedious but compliant with the security baseline that scanners check.
Compatibility. Chromium 88+, Firefox 74+ (partial; many features behind preference), Safari 16.4+. The header is forward-compatible: features the browser does not understand are silently ignored.
Common Mistakes. Using the old Feature-Policy syntax — semicolons between features, no quotes around URLs — which is silently ignored by browsers that have moved to Permissions-Policy. Omitting
fullscreen=(self) and then being surprised that a video player no longer allows full-screen. Restricting clipboard-write=() and breaking copy-to-clipboard buttons that were working a release earlier.Official reference: MDN: Permissions-Policy, W3C Permissions Policy.
3.8 Referrer-Policy
Purpose / Threat Model. Referrer-Policy controls how much of the current URL is sent as theReferer header when the browser navigates away from the page or fetches a cross-origin subresource. The default in modern browsers is strict-origin-when-cross-origin, which already prevents the full URL (including query string, which may carry session tokens or user IDs) from being leaked to third parties. Tightening it further is one of the cheapest privacy-and-security wins available.Syntax.
Referrer-Policy: <directive> [, <directive>]
| Directive | Behavior |
|---|---|
no-referrer | Never send Referer |
no-referrer-when-downgrade | Send full URL, except HTTPS to HTTP |
origin | Send only the scheme plus host (no path or query) |
origin-when-cross-origin | Full URL same-origin, origin only cross-origin |
same-origin | Full URL same-origin, nothing cross-origin |
strict-origin | Origin only, and nothing on HTTPS to HTTP |
strict-origin-when-cross-origin | Default; combines strict-origin and origin-when-cross-origin |
unsafe-url | Always send full URL (do not use) |
Recommended Value.
| Posture | Value |
|---|---|
| Modern Strict | no-referrer |
| Modern Permissive | strict-origin-when-cross-origin |
| Legacy | (header omitted; modern browsers default to strict-origin-when-cross-origin) |
no-referrer is appropriate when the site has no analytics that depend on referer information; strict-origin-when-cross-origin is the safe default that still lets analytics see the originating origin.Compatibility. Universal in modern browsers. Internet Explorer 11 supports only the legacy values (
no-referrer, unsafe-url); the strict-origin variants are silently treated as no-referrer-when-downgrade, which is benign.Common Mistakes. Setting
unsafe-url to support an analytics tool that wanted the full URL — this leaks tokens in the query string to every cross-origin link click. Not setting the header on a site that still serves an older browser population, where the in-browser default may be no-referrer-when-downgrade (full URL leakage).Official reference: MDN: Referrer-Policy.
3.9 X-Frame-Options
Purpose / Threat Model. X-Frame-Options declares whether the document may be displayed inside an<iframe>, <frame>, <embed>, or <object>. The threat it addresses is clickjacking, in which an attacker embeds your site inside their own page and tricks a logged-in user into clicking what looks like an innocent button but is actually a sensitive action on your site (transfer funds, change settings). X-Frame-Options predates CSP and is superseded by the CSP frame-ancestors directive, but it is still the safest choice on its own for legacy-browser compatibility and as defense-in-depth.Syntax.
X-Frame-Options: <directive>
| Directive | Behavior |
|---|---|
DENY | Cannot be framed by anyone |
SAMEORIGIN | Can be framed by same-origin documents only |
ALLOW-FROM <origin> | Allow a specific origin; obsolete |
Recommended Value.
| Posture | Value |
|---|---|
| Modern Strict | DENY |
| Modern Permissive | SAMEORIGIN |
| Legacy | SAMEORIGIN |
If you set CSP
frame-ancestors, X-Frame-Options becomes redundant in modern browsers but should still be sent for older browsers and as belt-and-braces.Compatibility. Universal.
ALLOW-FROM was never supported in Chrome and is officially obsolete in Firefox.Common Mistakes. Using
ALLOW-FROM (does not work in Chromium-based browsers). Setting X-Frame-Options but not setting CSP frame-ancestors, which means newer browsers will use one rule and older browsers the other, opening surprising gaps if the two disagree. Forgetting that SAMEORIGIN permits only direct same-origin embedding, not transitive embedding — if your same-origin page is itself embedded in a cross-origin iframe, the protection still holds.Official reference: MDN: X-Frame-Options, RFC 7034.
3.10 X-Content-Type-Options
Purpose / Threat Model. Browsers historically performed MIME sniffing — looking at the bytes of a response and guessing the content type even when theContent-Type header was wrong or absent. This was the source of an entire family of attacks in which an attacker would upload a file with a benign-looking extension but executable content, then trick the browser into treating it as a script or HTML. X-Content-Type-Options: nosniff disables this behavior: the browser must honor the declared Content-Type exactly.Syntax.
X-Content-Type-Options: nosniff
There is only one valid value.
Recommended Value.
nosniff on every response, no exceptions.Compatibility. Universal. Adding this header has zero compatibility risk.
Common Mistakes. Forgetting to set it on static-asset responses served from S3 or another object store, where the application layer never sees the request. Configuring it only on the HTML response, leaving file uploads and JSON APIs unprotected. Both attack scenarios assume an attacker can upload a file or control a response body, so the protection is meaningful only when the header is present everywhere.
Official reference: MDN: X-Content-Type-Options.
3.11 X-XSS-Protection (Deprecated)
Purpose / Threat Model. X-XSS-Protection used to enable browsers' built-in reflective-XSS filters. It is now deprecated and should not be sent. The filters it controlled were themselves a source of vulnerabilities — over its lifetime, Chrome's XSS Auditor was repeatedly shown to be coercible into selectively suppressing legitimate content or leaking information about the response body to an attacker on a sibling origin. Chromium removed the auditor in 2019, Edge followed, Firefox never implemented it, and modern guidance is to omit the header entirely or send0 to explicitly disable any vestigial behavior.Recommended Value.
| Posture | Value |
|---|---|
| Modern | (header omitted) or 0 |
| Legacy | 1; mode=block (no longer recommended) |
Compatibility. Removed from all browsers. The header is now a no-op.
Common Mistakes. Continuing to ship
X-XSS-Protection: 1; mode=block because a 2015-era hardening guide said to. There is no benefit and a small risk of confusion in audits.Official reference: MDN: X-XSS-Protection (marked deprecated).
3.12 Cache-Control (Security View)
Purpose / Threat Model. Cache-Control is primarily a performance header, but it has security implications when sensitive responses are stored in shared caches. A response carrying user-specific data — account balance, session token, personal email — must not be cached by an intermediary or a shared-browser environment such as a kiosk. The wrong cache directive leaks state across users.Syntax. Cache-Control is rich; the security-relevant directives are:
| Directive | Behavior |
|---|---|
no-store | Do not cache anywhere, ever |
no-cache | May cache, but must revalidate every time |
private | May cache in the user's browser, not in shared caches |
public | May cache in shared caches |
must-revalidate | If stale, must revalidate before serving |
max-age=<seconds> | TTL |
Recommended Value.
| Response Type | Header |
|---|---|
| Sensitive (logged-in HTML, JSON API) | Cache-Control: no-store |
| Static assets (hashed filenames) | Cache-Control: public, max-age=31536000, immutable |
| Static assets (non-hashed) | Cache-Control: public, max-age=3600, must-revalidate |
| HTML for static sites | Cache-Control: public, max-age=300, must-revalidate |
no-store is stronger than private and is the correct choice for any response that should never persist anywhere outside the originating server.Compatibility. Universal.
Common Mistakes. Using
Cache-Control: no-cache for sensitive responses; this still allows storage, just requires revalidation. The correct directive is no-store. Setting public on responses that vary by Authorization without including Vary: Authorization, so a CDN returns user A's cached copy to user B.Official reference: MDN: Cache-Control, RFC 9111.
3.13 Set-Cookie (Secure, HttpOnly, SameSite, Partitioned)
Purpose / Threat Model. The Set-Cookie header attaches a cookie to the response, and four attributes on the cookie govern how the browser treats it for security purposes. Together they defend against session hijacking, CSRF, and cross-site tracking. They are not separate headers, but the security headers space cannot be discussed without them.Syntax.
Set-Cookie: name=value; Secure; HttpOnly; SameSite=<Strict|Lax|None>; Partitioned; Path=/; Max-Age=<seconds>
| Attribute | Effect |
|---|---|
Secure | Cookie sent only over HTTPS |
HttpOnly | Cookie inaccessible to JavaScript |
SameSite=Strict | Cookie sent only on same-site navigation |
SameSite=Lax | Default in Chromium; same-site plus top-level GET |
SameSite=None | Cookie sent on cross-site; requires Secure |
Partitioned | Cross-site cookie partitioned by top-level site (CHIPS) |
Recommended Value.
| Cookie Type | Attributes |
|---|---|
| Session cookie | __Host- prefix; Secure; HttpOnly; SameSite=Lax; Path=/ (or Strict if no cross-site flows) |
| CSRF token | __Host- prefix; Secure; SameSite=Strict; Path=/ (no HttpOnly — JS must read it) |
| Cross-site embed (legacy iframe SSO) | Secure; HttpOnly; SameSite=None; Partitioned (cannot use __Host- across origins) |
Partitioned is the new isolation attribute under the Cookies Having Independent Partitioned State (CHIPS) initiative; it keeps SameSite=None cookies usable for cross-site iframes while preventing cross-site tracking.Two cookie name prefixes provide extra guarantees that the browser enforces on the cookie store itself. A cookie whose name starts with
__Secure- is accepted only if it is set with the Secure attribute over HTTPS; a cookie whose name starts with __Host- is accepted only if it is set with Secure, Path=/, and no Domain attribute. The __Host- prefix is the strongest defense against cookie injection from a related-domain attacker, because it pins the cookie to a single origin and prevents a subdomain takeover from overwriting it. Use __Host- for session and CSRF cookies whenever the cookie does not need to be shared across subdomains:Set-Cookie: __Host-session=abc; Secure; HttpOnly; SameSite=Lax; Path=/
Compatibility.
Secure, HttpOnly, SameSite=Strict/Lax are universal. SameSite=None requires Secure since Chrome 80 (2020). Partitioned is Chromium 114+ and Firefox 125+; Safari does not yet implement it, but the attribute is ignored harmlessly. The __Host- and __Secure- name prefixes are universal in modern browsers (Chrome 49+, Firefox 50+, Safari 11.1+).Common Mistakes. Omitting
HttpOnly on session cookies, which lets any XSS read the session token directly. Setting SameSite=None without Secure, which causes the cookie to be silently rejected by Chromium. Forgetting that the default in Chromium changed to SameSite=Lax in 2020, which broke many third-party-cookie integrations that had implicitly relied on the previous default of None. Setting a __Host--prefixed cookie with a Domain attribute or a non-root Path, which causes the browser to silently reject the cookie at write time.Official reference: MDN: Set-Cookie, RFC 6265bis.
3.14 Clear-Site-Data
Purpose / Threat Model. Clear-Site-Data instructs the browser to delete browsing data — cookies, storage, cache, or even execution contexts — associated with the response's origin. It is the right header to send on a logout endpoint: in one round trip the browser drops the session cookie, evicts the IndexedDB user database, clears local and session storage, and invalidates the bfcache entry so the back button cannot restore the logged-in state.Syntax.
Clear-Site-Data: "cookies", "storage", "cache", "executionContexts"
Each value is a double-quoted string, comma-separated. The special value
"*" clears everything.Recommended Value.
| Endpoint | Value |
|---|---|
| Logout | "cookies", "storage", "cache", "executionContexts" |
| Privacy reset | "*" |
| Other endpoints | (header omitted) |
Compatibility. Chromium 61+, Firefox 63+, Safari 16.4+. Older browsers ignore the header, so an explicit cookie expiration on logout is still required as a fallback.
Common Mistakes. Sending Clear-Site-Data on every response, which wipes the cache on every navigation and destroys performance. Sending it on the response that redirects to the login page rather than the login page itself, which means the new login page also gets its cache cleared.
Official reference: MDN: Clear-Site-Data, W3C Clear Site Data.
3.15 Server-Timing (Information Leakage View)
Purpose / Threat Model. Server-Timing is a performance header used to pass server-side timing metrics to the browser for display in DevTools. From a security perspective it is a potential information-disclosure surface: if your application names timing entries likedb-prod-replica-us-east-1=15 or auth-internal-vpc-call=23, you are exposing internal architecture to every user. The header is not a vulnerability in itself, but it is one of the headers a careful security review will inspect.Syntax.
Server-Timing: <name>;dur=<milliseconds>;desc="<description>", ...
Recommended Value. Either omit the header in production or constrain entries to opaque names with no internal architecture leaks:
| Posture | Approach |
|---|---|
| Modern Strict | Omit in production |
| Modern Permissive | Allow only generic names (db, cache, total) |
| Legacy | (no change) |
Compatibility. Universal, but only exposed to scripts that request
timingAllowOrigin or are same-origin.Common Mistakes. Leaving development-mode verbose timing on in production. Including internal hostnames or service names in the
desc field.Official reference: MDN: Server-Timing.
3.16 Origin-Agent-Cluster
Purpose / Threat Model. Origin-Agent-Cluster, when set to?1, requests that the browser place the document in an origin-keyed agent cluster rather than the default site-keyed one. The effect is that the document is isolated from other same-site origins for memory, scheduling, and document.domain setting purposes. It improves Spectre-style cross-origin isolation at the agent-cluster level even without the full cross-origin-isolated state.Syntax.
Origin-Agent-Cluster: ?1
?1 is the structured-fields syntax for the boolean true. ?0 is false, equivalent to omitting the header.Recommended Value.
| Posture | Value |
|---|---|
| Modern Strict | ?1 |
| Modern Permissive | (omitted) |
| Legacy | (omitted) |
Compatibility. Chromium 88+, Firefox 109+, Safari 16.4+. Older browsers ignore it.
Common Mistakes. Setting Origin-Agent-Cluster on an origin that legitimately uses
document.domain to share state across subdomains; the header disables that mechanism. Confusing it with COOP — they address related but distinct isolation surfaces.Official reference: MDN: Origin-Agent-Cluster.
3.17 Reporting-Endpoints and Report-To
Purpose / Threat Model. Reporting-Endpoints (and its older sibling, Report-To) declares named URLs to which the browser will POST violation reports for CSP, COEP, Permissions-Policy, deprecation warnings, intervention warnings, and crash reports. The header is the modern unified successor to per-featurereport-uri directives, and it lets one endpoint receive structured JSON for many different reporting types.Syntax.
Reporting-Endpoints: <name>="<url>", <name>="<url>", ...
Example:
Reporting-Endpoints: csp-endpoint="https://example.com/_/csp-reports", default="https://example.com/_/reports"
In CSP, refer to the endpoint by name with
report-to csp-endpoint. The same name can be used by other reporting consumers such as Cross-Origin-Embedder-Policy-Report-Only.Recommended Value.
| Posture | Value |
|---|---|
| Modern Strict | At least one named endpoint configured |
| Modern Permissive | At least one named endpoint configured |
| Legacy | Report-To: {"group":"default", "max_age":10886400, "endpoints":[{"url":"..."}]} |
Reporting-Endpoints uses HTTP structured fields and is the future direction. Report-To uses a JSON-encoded value and is being phased out, but is still required for older Chromium versions; ship both during the transition.
Compatibility. Reporting-Endpoints: Chromium 96+, Firefox does not yet implement, Safari does not yet implement. Report-To: Chromium 69+. There is no harm in shipping both headers.
Common Mistakes. Pointing the reporting endpoint at an origin that does not accept POST with the
application/reports+json content type — the browser silently drops the report. Including the endpoint URL but never reading the reports.Official reference: MDN: Reporting-Endpoints, W3C Reporting API.
3.18 Expect-CT (Deprecated)
Purpose / Threat Model. Expect-CT instructed browsers to enforce Certificate Transparency for the host. It was relevant during the transition period (2018-2021) when CT enforcement was rolling out across CAs and browsers, but CT enforcement is now a baseline expectation, not an opt-in. The header is deprecated and removed from Chromium since version 107 (2022). It should not be sent.Recommended Value. Omit. Do not send
Expect-CT even if a 2019 hardening guide insisted on it.Compatibility. Removed from Chromium. The header is now a no-op.
Common Mistakes. Continuing to ship the header. There is no benefit.
Official reference: MDN: Expect-CT (deprecated and removed).
3.19 Public-Key-Pins (Deprecated)
Purpose / Threat Model. HTTP Public Key Pinning was an attempt to defend against rogue certificate authorities by pinning a site's expected public key fingerprint. It was deprecated in 2018 and removed from Chromium and Firefox shortly after because the failure mode — a small typo in the pin, or accidental key rotation, could brick the site for the entiremax-age of the pin — was unacceptable in practice. CT logs and the broader CA ecosystem have moved on. Do not send the header.Recommended Value. Omit. The replacement is to rely on Certificate Transparency, careful CA selection, and DNS CAA records.
Compatibility. Removed.
Common Mistakes. Re-enabling the header after an outage caused by an old pin. There is no modern use case.
Official reference: MDN: Public-Key-Pins (deprecated and removed).
3.20 Other Headers Worth a Mention
A handful of headers do not warrant their own section but appear often enough in audits to be worth noting briefly.Server. The Server header identifies the web server software and version. There is no security benefit to advertising
Server: nginx/1.27.1 or Server: Apache/2.4.62; you simply make the attacker's job easier. Reduce it to Server: nginx or remove it via server_tokens off; (nginx) or ServerTokens Prod (Apache).X-Powered-By. Same threat model as Server. Emitted by default by PHP, IIS, and several application frameworks. Disable wherever possible.
X-DNS-Prefetch-Control. Controls DNS prefetching. The default in modern browsers is on, and disabling it (
X-DNS-Prefetch-Control: off) is a privacy choice that hurts performance. Generally leave it alone.X-Permitted-Cross-Domain-Policies. Adobe Flash and Acrobat-era control over
crossdomain.xml. With Flash dead, setting X-Permitted-Cross-Domain-Policies: none adds defense in depth on sites that historically served crossdomain.xml, and costs nothing.Origin-Trial. Used by browsers to enable experimental APIs. Not a security header per se; do not strip it without checking which feature it gates.
4. CSP Deep Dive - Directives, Nonces, Hashes, and Strict Dynamic
Content Security Policy deserves more depth than the per-header section allows, because the difference between a CSP that meaningfully prevents XSS and a CSP that exists to satisfy a scanner is large, and the dividing line runs through a handful of decisions that this section walks through.4.1 Nonces, Hashes, and Strict-Dynamic
A nonce is a per-response random value, generated server-side, that the page emits both in the CSP header and on every legitimate inline<script> tag:Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'
<script nonce="r4nd0m">/* legitimate inline script */</script>
The browser executes the inline script because the nonce matches; an attacker who injects another inline script cannot guess the nonce (a fresh one is generated per response, ideally 128 bits of entropy) so their script is blocked. Nonces are far more flexible than URL allowlists because they let you ship inline bootstrap code — exactly the kind of code that frameworks like Next.js, Remix, Astro, and SvelteKit emit during hydration — without resorting to
'unsafe-inline'.A hash is the SHA-256 (or SHA-384/SHA-512) digest of an inline script's body, expressed in CSP as
'sha256-<base64>'. Hashes are preferable to nonces when the inline content is static across responses, because they require no per-response server work. The price is that any change to the script body changes the hash, so hashes pair badly with build tools that inline rapidly-evolving code.'strict-dynamic' extends the trust placed in a nonced or hashed script to any further scripts that script loads dynamically (via document.createElement('script') or similar). This is the unlock that makes nonces practical: you nonce a single bootstrap script, and from there the bootstrap can load the rest of the application without each subsequent script needing its own nonce. The trade-off is that 'strict-dynamic' ignores URL allowlists in script-src, so if you specify script-src 'nonce-abc' 'strict-dynamic' https://cdn.example.com, the https://cdn.example.com part is silently ignored in browsers that understand 'strict-dynamic'. In Chromium and Firefox this is the desired modern behavior; older browsers fall back to URL allowlist enforcement.4.2 Trusted Types
Trusted Types are a W3C WebAppSec feature (now part of CSP Level 3) implemented in Chromium and Firefox 135+ that turns DOM-injection sinks likeinnerHTML, outerHTML, and eval into type-checked operations: the value assigned must be an explicit Trusted Type object, not a plain string. The mechanism is opted in via require-trusted-types-for 'script', which is a CSP directive, and the allowed policy names are declared with trusted-types.Trusted Types is the most effective defense against DOM-based XSS available today, because it forces every dangerous sink through a deliberate, auditable factory function. It is also the highest-effort feature in this article to adopt: any code path that touches
innerHTML must be refactored to use a Trusted Types policy. For a site with a long history of DOM manipulation, the first audit is usually painful.Trusted Types should be a goal, not an immediate requirement. Ship CSP nonces with
'strict-dynamic' first, get to zero CSP violations in report-only, then turn on Trusted Types as a second phase.4.3 CSP and frame-ancestors
The CSPframe-ancestors directive is the modern equivalent of X-Frame-Options. When both are present and the browser supports CSP Level 2, the browser uses frame-ancestors and ignores X-Frame-Options. The two should be kept consistent — sending frame-ancestors 'self' while X-Frame-Options says DENY will not break anything, but it is the kind of inconsistency that confuses audit reviewers.A useful corner case:
frame-ancestors understands source lists, so frame-ancestors 'self' https://partner.example.com is a valid policy that allows one specific cross-origin partner to embed. X-Frame-Options never supported this expressively because ALLOW-FROM was never implemented in Chromium.4.4 Report-Only Rollout Strategy
The recommended path to production CSP enforcement is:- Ship
Content-Security-Policy-Report-Onlywith the policy you intend to enforce, paired with a Reporting-Endpoints (or Report-To) endpoint. - Collect reports for at least two weeks — enough to see one full release cycle, weekly cron jobs, and the long tail of regional users with unusual browser extensions.
- Triage reports into "legitimate but missed" (add the source to the allowlist or refactor to nonce-based loading), "legitimate inline" (move to nonce or hash), "browser extension noise" (ignore), and "real attack attempts" (alert).
- Promote the policy to the enforcing
Content-Security-Policyheader while keeping report-only running for the next iteration.
The biggest single mistake during a rollout is to attempt to ship the enforcing header on day one. Reports from your own dev environment never approximate the diversity of real-user environments, and an over-tight policy breaks real users while you sleep.
4.5 CSP and Inline Event Handlers
A CSP that disallows'unsafe-inline' in script-src also disallows inline event handlers like onclick="...". The directive script-src-attr controls this specifically; setting script-src-attr 'none' is a clean way to say "no inline event handlers, ever." Modern frameworks generate event handlers via JavaScript, so this is rarely a regression, but legacy templates and email-rendered HTML may break and need refactoring.4.6 CSP for Static Sites
A pure static site has the easiest CSP because nonces are not available (no server-side render). The strict policy in that case uses hashes for the small set of inline scripts the site needs, plus an allowlist for any CDN-hosted third-party scripts:Content-Security-Policy:
default-src 'self';
script-src 'self' 'sha256-<hash>' 'strict-dynamic';
style-src 'self' 'sha256-<hash>';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'none';
form-action 'self';
object-src 'none';
upgrade-insecure-requests
The CI build calculates the SHA-256 of any inline
<script> content and injects the hash into the CSP at deploy time. Tools like the OWASP CSP Evaluator can validate the resulting policy.5. Reference Configurations for SPA, SSR, and Static Sites
The header recommendations above are deliberately split by posture rather than by application type. This section pulls them back together into three end-to-end reference configurations.5.1 Configuration A: Single-Page App (React, Vue, or Svelte on an API backend)
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'nonce-{random}' 'strict-dynamic'; style-src 'self' 'nonce-{random}'; img-src 'self' data: https://images.example.com; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; object-src 'none'; require-trusted-types-for 'script'; upgrade-insecure-requests; report-to csp-endpoint
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: accelerometer=(), camera=(), geolocation=(), microphone=(), payment=(), usb=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Origin-Agent-Cluster: ?1
Reporting-Endpoints: csp-endpoint="https://example.com/_/csp-reports"
Cache-Control: no-store (on authenticated HTML and API responses)
This configuration assumes the SPA renders an initial HTML shell that ships an inline bootstrap script (nonced) which then loads the rest of the bundle. The
connect-src lists the API origin explicitly.5.2 Configuration B: Server-Rendered App (Rails, Django, Laravel, or Next.js)
The headers are the same as Configuration A, with two differences. First, server-rendered apps usually emit nonced inline scripts for hydration data, so the nonce-based CSP is a perfect fit. Second, server-rendered apps often serve files of mixed cacheability — long-lived static asset URLs and short-lived dynamic HTML — and the Cache-Control varies per route accordingly:Cache-Control: no-store (on authenticated HTML)
Cache-Control: public, max-age=300, must-revalidate (on public HTML)
Cache-Control: public, max-age=31536000, immutable (on /static/<hash>.js)
5.3 Configuration C: Pure Static Site (S3 plus CloudFront, Netlify, Cloudflare Pages)
Static sites lose the ability to inject per-response nonces, so the CSP must use either hashes (for stable inline content) or no inline script at all (move everything to external files). Static sites also have the simplest header surface because every response goes through the same edge configuration, so getting headers right at the edge propagates everywhere.Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-{computed-at-build}'; style-src 'self' 'sha256-{computed-at-build}'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; object-src 'none'; upgrade-insecure-requests
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: accelerometer=(), camera=(), geolocation=(), microphone=(), payment=(), usb=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Origin-Agent-Cluster: ?1
Cache-Control: public, max-age=300, must-revalidate (on HTML)
Cache-Control: public, max-age=31536000, immutable (on hashed JS/CSS/fonts)
For an end-to-end walkthrough of the static-site stack that hosts this kind of configuration, the Web Performance Checklist for Core Web Vitals covers the Cache-Control side, and the PWA Advanced Implementation Guide covers the service-worker layer that also needs to respect these headers.
5.4 Implementation Note: CDN vs Application Layer
Whether headers are applied at the CDN edge, at a reverse proxy, or in application middleware is more a matter of taste than security — what matters is that they are applied somewhere consistent. The trade-offs:| Layer | Pros | Cons |
|---|---|---|
| CDN / edge function | One config covers every route and asset | Cannot use per-request nonces (without per-request edge compute) |
| Reverse proxy (nginx, Caddy) | Per-route control; can read upstream headers | Header injection logic lives outside the application |
| Application middleware | Can compute nonces; per-route control | Headers may not be applied to static assets served by CDN |
The most common production pattern is CSP nonces in application middleware, all other headers at the CDN edge. That gives you the per-request nonce CSP needs without forcing every header to live in application code.
6. Tool: HTTP Security Header Analyzer
Reading this reference top to bottom gives you the values; the HTTP Security Header Analyzer Tool on this site gives you the verification loop. The tool accepts a paste ofcurl -I output or DevTools-copied response headers, parses out the headers it recognizes, and produces:- A numeric score between 0 and 100, weighted across ten security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, COOP, COEP, CORP, and Set-Cookie flags).
- A letter grade from A to F.
- Per-header findings with Pass/Warn/Fail/Info badges, the observed value, and a one-line recommendation that mirrors the guidance in this article.
- A parsed-headers table showing every header the analyzer saw, in original order, which is useful for confirming that a header you thought was being sent is actually present.
The intended workflow is iterative. Capture headers from a real response, paste, fix the most prominent Fail, re-deploy, re-capture, re-paste. The tool is entirely client-side; pastes never leave the browser. Three preset examples — Vulnerable, Partial, and Hardened — let you see the scoring against known baselines before you paste real data.
A few usage tips that come up in practice. First, the parser is tolerant of HTTP/2 pseudo-headers and the response status line, so you can paste the raw
curl -I -L output as-is. Second, the parser implements RFC 7230 line folding, so multi-line CSP values pasted from curl -I will be re-joined automatically. Third, if a header appears multiple times (legitimate for Set-Cookie), all instances are evaluated. Fourth, the Copy Report button serializes the analysis as plain text suitable for pasting into a code review comment or a security ticket.The deliberate non-feature of the tool is that it does not fetch URLs. Cross-origin fetches are unreliable from a static site because of CORS restrictions, and a tool that lies about which headers a site actually returns is worse than no tool at all. The paste-only workflow is more work, but the answer it returns is the answer the browser would see.
For deeper context on the rest of the toolchain that supports this kind of static-site security work, see the Amazon S3 Object Key Design Best Practices article, which covers the CDN-side configuration where most edge headers are set, and the AWS History and Timeline regarding Amazon CloudFront article, which traces the response-header policy features back through CloudFront's evolution.
7. Frequently Asked Questions
Do I need every one of these headers?Six of them are essential on every HTTPS site: HSTS, CSP, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and X-Frame-Options (or CSP
frame-ancestors). Three more — COOP, COEP, CORP — are required if you want cross-origin isolation; otherwise CORP alone is still worth setting on private resources. Cache-Control is essential per-route. The deprecated three (X-XSS-Protection, Expect-CT, Public-Key-Pins) should be explicitly removed if any older configuration still emits them.Can I set headers in the HTML meta tag instead of in HTTP?
A subset of headers — most usefully
Content-Security-Policy and Referrer-Policy — accept an HTML <meta http-equiv="..." content="..."> equivalent. X-Content-Type-Options, HSTS, X-Frame-Options, and the cross-origin trio do not — setting X-Content-Type-Options: nosniff via <meta> is silently ignored by browsers and must be sent as a true response header. Meta-tag headers also miss every non-HTML response (JSON, images, fonts), so they are best treated as a fallback only.Why does my CSP nonce work in development but break in production?
The most common cause is that production caches the HTML, so the same nonce is served to many users — and a sufficiently determined attacker can predict it. Always pair nonce-based CSP with
Cache-Control: no-store (or private, max-age=0, must-revalidate) on HTML responses, and regenerate the nonce per response.Why is X-Frame-Options SAMEORIGIN not enough?
It is the minimum, and on its own it is fine for most sites. The CSP
frame-ancestors directive is more expressive (it can list multiple allowed embedders) and is the future direction. Set both for defense in depth.Why does Safari ignore some of these headers?
Safari historically lagged on the cross-origin trio (COOP/COEP/CORP) and on Permissions-Policy. As of Safari 16.4 (2023) most of the gap has closed, but
credentialless for COEP and Partitioned for cookies are still Chromium-and-Firefox-only. Always treat the cross-origin headers as best-effort on Safari.What about HTTP/3 and headers?
HTTP/3 carries response headers identically to HTTP/2; the same security header values apply. The only protocol-level difference is that HTTP/3 connections start on UDP via QUIC, and the Alt-Svc header is how a server advertises HTTP/3 support. Alt-Svc is not a security header, but it does interact with HSTS in that an upgrade to HTTPS-only is required for Alt-Svc to be honored.
Should I send Content-Security-Policy as a real header or via meta?
Always use the HTTP header when possible. The
<meta> form does not support frame-ancestors, sandbox, report-uri, or report-to, all of which are essential for production use. The <meta> form is also evaluated late, after the page has begun parsing, which means some early-loading scripts may evade the policy.Does sending a Server header expose me to vulnerabilities?
Indirectly. Knowing the exact server version makes it cheaper for an attacker to target known CVEs; obscuring the version raises the cost of reconnaissance. Reduce or remove the Server header on production responses.
How often should I re-audit my headers?
At least once a quarter, and immediately after any CDN, reverse-proxy, or application framework upgrade. Browser support for header features evolves continuously; what was state of the art six months ago may have a tighter recommendation today. Paste the latest response into the HTTP Security Header Analyzer Tool to spot regressions quickly.
8. Summary
HTTP security headers are declarative, server-emitted defenses that the browser enforces at the user-agent layer, eliminating entire vulnerability classes at near-zero cost. Six headers form the baseline for any HTTPS site — HSTS, CSP, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and X-Frame-Options (or CSPframe-ancestors) — with the cross-origin trio (COOP, COEP, CORP) layered on top when cross-origin isolation is required. Three deprecated headers (X-XSS-Protection, Expect-CT, Public-Key-Pins) should be explicitly removed wherever older configurations still emit them.The single highest-leverage upgrade for most sites is moving CSP from an allowlist to a nonce-based policy with
'strict-dynamic', deployed first as Content-Security-Policy-Report-Only and promoted to enforcement only after two weeks of real-user reports. Pair nonce-based CSP with Cache-Control: no-store on HTML so the nonce is not cached across users, and graduate to Trusted Types once CSP is stable.Treat header hygiene as a recurring task, not a one-time setup. Re-audit after every CDN, reverse-proxy, or framework upgrade, and use the HTTP Security Header Analyzer Tool to verify each iteration against the recommendations in this reference.
9. References
- MDN Web Docs — HTTP headers (the canonical per-header reference)
- OWASP — Secure Headers Project
- OWASP — Content Security Policy Cheat Sheet
- W3C — Content Security Policy Level 3
- W3C — Permissions Policy
- W3C — Reporting API
- W3C — Clear Site Data
- IETF RFC 6797 — HTTP Strict Transport Security
- IETF RFC 9111 — HTTP Caching
- web.dev — Cross-Origin Isolation Overview
- web.dev — Strict CSP
- Google — HSTS Preload List Submission
Related Articles in This Series
- HTTP Security Header Analyzer Tool — the companion client-side tool that grades the headers from this reference against a pasted response.
- CloudFront KeyValueStore and Edge Functions Patterns — the CloudFront-side layer where most production security headers are injected at the edge.
- CloudFormation Stack for ACM, Lambda@Edge, WAF, S3, and CloudFront — the end-to-end infrastructure pattern that delivers HTTPS, WAF, and edge-applied headers for a static site.
- AWS History and Timeline of Amazon CloudFront — the response-header policy and origin-request features in CloudFront traced through their evolution.
- AWS Verified Permissions and Cedar Complete Guide — the application-layer authorization story that picks up where transport- and document-layer header defenses leave off.
References:
Tech Blog with curated related content
Written by Hidekazu Konishi