Skip to content

Test your website now

Free, no card. We read only public pages.

Run free audit →

Security

Security headers: what each one does, and what it breaks

What HSTS, CSP, X-Frame-Options, nosniff, Referrer-Policy and Permissions-Policy actually do, the order to add them in, and what each one breaks on the way.

Published

What a security header actually is

A security header is a line in an HTTP response that tells the browser to refuse something. That is the whole idea, and it is why they are worth the afternoon: the browser is the only party in the transaction that is not under an attacker's control, and these headers are the instructions it will follow even when your page has already been compromised.

They are also the cheapest security work that exists. Nothing is rewritten, nothing is deployed differently, no dependency is upgraded. Most of what follows is a single line wherever your responses are assembled - the web server, the CDN, or one middleware in whatever framework you use.

None of these headers fixes a vulnerability. They limit what a vulnerability can do. A site with a perfect header set and an unauthenticated admin endpoint is an insecure site with good headers - see the last section for what this class of check cannot tell you.

The order to add them in

Add them in increasing order of how likely they are to break something. Almost every team that tries to do this in one go starts with Content-Security-Policy, breaks their analytics, and reverts the entire change including the six headers that were harmless.

  • X-Content-Type-Options - cannot break a correctly served site at all
  • Referrer-Policy - changes what other sites see in their logs, not what yours does
  • X-Frame-Options or frame-ancestors - breaks only intentional embedding, which you know about
  • Strict-Transport-Security - safe once HTTPS genuinely works everywhere, and hard to undo
  • Permissions-Policy - breaks only features you deliberately allow back in
  • Content-Security-Policy - the one that takes a week, and the one worth it

X-Content-Type-Options: nosniff

Browsers used to guess a response's type when the Content-Type looked wrong. That guessing is what turns a user-uploaded file that claims to be a PNG into a script the browser executes. nosniff turns the guessing off: the declared type is the type, and a mislabelled file simply fails to load instead of running.

X-Content-Type-Options: nosniff

There is no configuration and no tuning. Set it globally at the server or CDN, on every response and not only on HTML. The only sites it breaks are ones that were relying on the guessing - which is the same thing as saying they were serving files with the wrong Content-Type, and that is worth finding out about.

Referrer-Policy

By default, when someone clicks a link out of your site, the destination is told the full URL they came from. On a marketing page that is harmless. On /reset-password?token=… or /invoices/8213 it is a credential or a customer's data handed to whoever they clicked through to, and it lands in a third party's log file where you will never see it again.

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

That value sends the full URL within your own site, sends only the origin to other HTTPS sites, and sends nothing at all when downgrading to HTTP. It is what modern browsers already default to, and setting it explicitly means you do not depend on a default that older clients do not share.

If you run affiliate tracking or an analytics setup that reads the referrer path from other sites, check it before you tighten this. It is the one header on this list whose breakage shows up as a slow drift in a dashboard rather than an error.

Clickjacking: X-Frame-Options and frame-ancestors

Clickjacking is your site loaded in an invisible iframe on somebody else's page, positioned so that the button the visitor thinks they are clicking is actually yours. It is old, it is still effective, and the defence is a single line telling browsers who may frame you.

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

frame-ancestors is the modern form and it is what you should add. X-Frame-Options: SAMEORIGIN is the older header and is still worth sending alongside it for clients that do not implement CSP - the two do not conflict, and where both are present browsers that understand frame-ancestors ignore the older one.

The important detail: frame-ancestors is the one CSP directive you can adopt on its own. You do not need a full policy to use it, so a site that is not ready for CSP can still close clickjacking today.

Strict-Transport-Security (HSTS)

Having a valid certificate does not mean visitors arrive over HTTPS. Somebody typing your domain makes a plain HTTP request first, and an attacker on the same network can answer it before your redirect does. HSTS fixes the second visit and every visit after: once the browser has seen the header, it upgrades to HTTPS itself and refuses to be talked out of it.

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

A year is the conventional value. Anything under six months is weak enough that we grade it as a warning rather than a pass, because the whole protection is the browser remembering - a max-age of a few days expires between one visit and the next for most people.

This is the one header on the list that is genuinely hard to undo. includeSubDomains means every subdomain must serve valid HTTPS, including ones you forgot about, and a browser that has cached the directive will not let a user click through to reach them. Enumerate your subdomains before you add it, and start without includeSubDomains if you are not certain.

Preloading - submitting your domain to the list browsers ship with - closes the very first visit too. Do it only once the header has been live and stable for months, because removal from the list takes effect at browser-release speed, not at deploy speed.

Permissions-Policy

Permissions-Policy declares which powerful browser features your pages may use: camera, microphone, geolocation, payment, and a long tail of others. Its value is not that it protects you from yourself - it is that it applies to every third-party script and every iframe on the page too.

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

An empty list means nobody, including your own origin. Start there for the features you genuinely do not use, which for most sites is nearly all of them, and add back only what a feature actually needs. It is low severity on its own and it is five minutes of work.

Content-Security-Policy, the one worth the week

CSP is the difference between a cross-site scripting bug being an incident and being a blocked console message. It tells the browser which sources of script, style, image and frame content are legitimate, and everything else does not execute - including the script an attacker just managed to inject into your page.

It is also the only header here that will break your site if you write it in one sitting and enforce it immediately. The way to do this without an outage is three steps, and the middle one is the one people skip:

  • Deploy it as Content-Security-Policy-Report-Only. The browser reports what the policy would have blocked and blocks nothing.
  • Leave it there for at least a week of real traffic, including a marketing campaign and a full release cycle, and read the reports. This is where you discover the tag manager, the two analytics vendors and the payment widget that nobody documented.
  • Only then rename the header to Content-Security-Policy.

A reasonable starting policy for a site that has none, to be tightened rather than shipped as-is:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-{random-per-request}';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'self'

The three directives people leave out are the three that matter most in practice. object-src 'none' removes the plugin surface entirely and breaks nothing on a modern site. base-uri 'self' stops an injected <base> tag from silently repointing every relative URL on the page, which is a real and under-appreciated way to defeat an otherwise careful script-src. And frame-ancestors, as above, replaces X-Frame-Options.

The one thing that quietly makes a CSP worthless: 'unsafe-inline' in script-src. A policy with it allows exactly what CSP exists to prevent. The correct fix is a per-request nonce on every inline script - and when a nonce or a hash is present, browsers ignore an accompanying 'unsafe-inline', which is why the two together are a legitimate compatibility pattern rather than a mistake. 'unsafe-eval' is the same problem for a different family of functions.

How to verify you actually shipped them

Headers are configured in one place and served from another, and the two disagree more often than anyone expects: a CDN strips them, a caching layer serves a response that predates the change, or the rule matches HTML but not the API routes on the same domain. Check the response, not the config.

curl -sSI https://example.com | grep -i -E 'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy'

Then check a page that is not the homepage, a response that is not HTML, and - if you have one - a subdomain. Header rules scoped to the wrong location are the single most common reason a site that believes it is protected is not.

What headers cannot tell you

This is the part most header checklists leave out, and it is the part that decides whether an audit is honest. Every header above is visible from outside, which is exactly why it is easy to check - and everything that matters and is not visible from outside stays invisible no matter how many scanners you run.

  • Whether passwords are hashed, and with what
  • Whether your API authorises requests or merely authenticates them
  • Whether rate limiting exists on the endpoints that need it
  • Whether row-level access control is enforced in the database or assumed in the application
  • Whether your backups restore

A perfect header score means the browser-side controls are in place. It is not a security assessment, and any tool that presents it as one - ours included - is selling a green tick rather than an answer. The useful thing a scanner can do is measure what it can measure, say plainly which questions it did not answer, and leave those on a list somebody has to sign off by hand.

In the product

The checks this maps to

Every check named here runs on every scan, on every plan - what a plan changes is how many pages are crawled, how long the history is kept and what you can do with the result. These are the ids the findings carry, so you can match a line in a report to the explanation above.

  • sec.headers.hsts - Strict-Transport-Security header
  • sec.headers.csp - Content-Security-Policy header
  • sec.headers.xfo - Clickjacking protection (X-Frame-Options / frame-ancestors)
  • sec.headers.nosniff - X-Content-Type-Options: nosniff
  • sec.headers.referrer-policy - Referrer-Policy header
  • sec.headers.permissions-policy - Permissions-Policy header
  • sec.headers.coop - Cross-Origin-Opener-Policy header
  • sec.csp.unsafe-eval - CSP does not allow 'unsafe-eval'
  • sec.csp.default-src - CSP declares a default-src fallback
  • sec.https.redirect - HTTP redirects to HTTPS