Security headers, ranked by what they actually stop
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
X-XSS-Protection— the legacy browser XSS filter it controlled has been removed from every major browser, and in some versions the filter itself introduced vulnerabilities. Set it to0or drop it. Do not set it to1; mode=block.ServerandX-Powered-Bywith version numbers — not a vulnerability, but they hand an attacker a list of CVEs to try without them having to probe for it.
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'.