← All posts

Security headers, ranked by what they actually stop

6 September 2026 · 7 min read · Web security

Every security scanner will hand you a list of missing headers. Not many explain which ones prevent a real attack and which are box-ticking. The difference matters, because adding all of them badly is worse than adding three of them properly.

Here they are in the order I would fix them.

1. Content-Security-Policy

This is the only header on the list that stops cross-site scripting from executing. Everything else on the page is a smaller control.

A policy restricts where scripts, styles, images and connections may come from. If an attacker injects a <script> tag into your page, a strict policy means the browser refuses to run it.

The catch, and it is a large one:

Content-Security-Policy: script-src 'self' 'unsafe-inline'

'unsafe-inline' permits any inline script — including the one the attacker just injected. A policy containing it provides essentially no XSS protection, while still scoring as "CSP present" on most scanners. This is the single most common way a policy looks strict and blocks nothing.

Doing it properly means either a nonce or a hash for each legitimate inline script:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m';
  object-src 'none'; base-uri 'none'; frame-ancestors 'none'

The nonce must be regenerated per response and be unguessable. Two extras worth knowing: base-uri 'none' stops an injected <base> tag redirecting every relative URL on the page, and object-src 'none' closes off plugin content.

Deploy with Content-Security-Policy-Report-Only first. It reports violations without blocking, so you find out what you would have broken before you break it.

2. Strict-Transport-Security

HSTS tells a browser to only ever reach your site over HTTPS, for a stated period. Without it, the first request a user makes — typing the domain, or following an old http:// link — goes out in plaintext, where it can be intercepted and downgraded before your redirect ever arrives.

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

A short max-age is a common half-measure: 86400 leaves a one-day window every time it lapses. A year is the norm.

includeSubDomains matters more than it looks. Without it, a subdomain served over plain HTTP can set a cookie for the parent domain. preload ships your domain in browsers so even the very first visit is protected — but read the requirements before adding it, because removal from the preload list takes months.

3. X-Content-Type-Options

X-Content-Type-Options: nosniff

One value, no configuration, no way to get it wrong. It stops browsers guessing a response's type from its contents rather than its Content-Type. Without it, a user-uploaded file served as text/plain can be sniffed as JavaScript and executed.

Best effort-to-benefit ratio on the list. There is no reason not to have it.

4. Clickjacking: frame-ancestors, not X-Frame-Options

Clickjacking loads your site in an invisible frame over an attacker's page so the victim clicks your buttons while thinking they are clicking something else.

The modern control is a CSP directive:

Content-Security-Policy: frame-ancestors 'none'

It supersedes X-Frame-Options, and unlike the old header it can express a list of permitted origins. Where both are present, browsers that understand frame-ancestors use it and ignore X-Frame-Options. Keeping the old header alongside is harmless for older clients.

5. Referrer-Policy

Referrer-Policy: strict-origin-when-cross-origin

Not an attack control — a leak control. The default behaviour sends the full URL of the referring page to third parties. If your URLs contain password reset tokens, session identifiers, internal ticket numbers or search terms, they are being handed to every external resource the page loads.

The value above sends the full URL within your own origin and only the origin when leaving it, which is the right balance for most sites.

6. Permissions-Policy

Permissions-Policy: geolocation=(), camera=(), microphone=()

Switches off browser features your site does not use, for your page and anything it embeds. Modest benefit on its own, but it reduces what injected or third-party code can reach for.

Two you should remove

The part people miss

Headers must be present on every response, not just the home page. Error pages, redirects, API responses and static files are frequently served by a different path through your stack, and that is exactly where the headers go missing. If your framework sets them in middleware, a static file handler that short-circuits it will not have them.

Check a 404 and an asset URL, not only /.

The Website Security Scanner checks all of these on any site, and reports what each header actually contains rather than just whether it is present — including whether a CSP is weakened by 'unsafe-inline'.