← All articles
Hardening

HTTP security headers explained: the six that matter

What Content-Security-Policy, HSTS, X-Frame-Options and friends actually protect against — and how to deploy them without breaking your site.

HTTP response headers are instructions your server sends the browser alongside every page. A handful of them tell the browser to enforce security rules it would otherwise skip. They are cheap to deploy, they apply to every visitor automatically, and getting them wrong is one of the most common findings on otherwise well-run sites. This is a tour of the six that matter, what each one actually stops, and the mistakes that quietly turn them into decoration.

One framing to keep in mind throughout: security headers are defence-in-depth, not a fix. A Content-Security-Policy reduces the damage a cross-site scripting bug can do — it does not remove the bug. Treat these headers as the seatbelt, not the reason to drive carefully.

Content-Security-Policy (CSP)

Mitigates: the impact of cross-site scripting (XSS) and content injection. A CSP tells the browser which sources it is allowed to load scripts, styles, images and frames from. Even if an attacker manages to inject markup into your page, a strict policy means the browser refuses to execute an inline <script> or fetch code from an attacker-controlled host.

Safe example:

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

The gotcha: adding 'unsafe-inline' or 'unsafe-eval' to script-src defeats most of the point. 'unsafe-inline' re-permits exactly the inline-script execution that XSS relies on. Wildcards like script-src * or a bare https: scheme source are almost as bad — they let scripts load from anywhere. If your site needs inline scripts, move to a nonce- or hash-based policy rather than opening the door for everything. CSP is also easy to ship in a broken-but-quiet state, so roll it out with Content-Security-Policy-Report-Only first, watch what it would have blocked, then enforce.

What it does not do: CSP does not sanitise your inputs or patch the injection flaw. It limits the blast radius. A site can have a flawless CSP and still be vulnerable — the policy just makes the vulnerability far less useful to an attacker.

Strict-Transport-Security (HSTS)

Mitigates: protocol downgrade and SSL-stripping man-in-the-middle attacks. HSTS tells the browser to only ever contact your domain over HTTPS, for a set duration, and to refuse to connect if the certificate is invalid. Without it, a user who types yourdomain.com makes a first request over plain HTTP that an attacker on the network can intercept before the redirect to HTTPS ever happens.

Safe example:

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

The gotcha: includeSubDomains and preload are effectively one-way doors. includeSubDomains forces every subdomain to be HTTPS-only — if you have an internal or legacy host that still serves plain HTTP, it will break. preload bakes your domain into a list shipped inside browsers themselves; removal is slow and out of your hands. Start with a short max-age (say a few minutes) to confirm nothing breaks, then raise it. Only add includeSubDomains and preload once you are certain every subdomain is ready.

What it does not do: HSTS does not encrypt anything itself — TLS does that. It also cannot protect the very first visit to a domain the browser has never seen (the trust-on-first-use gap), which is exactly what the preload list closes. And it is meaningless if your certificate or protocol configuration is weak in the first place; see deprecated TLS versions for the layer underneath it.

X-Frame-Options

Mitigates: clickjacking. This header controls whether your page can be embedded inside a <frame>, <iframe> or <object> on another site. In a clickjacking attack, your real page is loaded invisibly over a decoy so that a victim's click lands on your "delete account" or "transfer" button without them realising.

Safe example:

X-Frame-Options: DENY

Use SAMEORIGIN instead if your own application legitimately frames its own pages.

The gotcha: X-Frame-Options is the older mechanism and cannot express "allow these two specific partners to frame me." The modern replacement is the CSP directive frame-ancestors, which browsers honour in preference to this header. Set both for coverage — frame-ancestors 'self' in your CSP plus X-Frame-Options: SAMEORIGIN — but make sure they agree, or you will confuse yourself later.

What it does not do: nothing beyond framing. It is not a general anti-embedding or anti-scraping control.

X-Content-Type-Options

Mitigates: MIME sniffing. Some browsers historically tried to guess a response's content type when it looked wrong, which let an attacker smuggle executable script through a file the server labelled as something harmless (an uploaded "image", for instance). This header switches that guessing off.

Safe example:

X-Content-Type-Options: nosniff

The gotcha: nosniff only helps if your server actually sends correct Content-Type headers. If you serve a stylesheet as text/plain, a strict browser will now refuse to load it. The header is the easy part; getting content types right across your responses is the work.

What it does not do: it does not validate uploads or scan file contents. It removes one specific browser behaviour that turned mislabelled files into a code-execution path.

Referrer-Policy

Mitigates: referrer leakage. The Referer header tells the next site which page a request came from. Modern browsers already default to strict-origin-when-cross-origin, so they send only your bare origin — not the full path or query — to other sites. The exposure is when that default is undone: an older client, or a server or framework configured with a weaker legacy policy, that sends the full URL. If your URLs carry session tokens, reset tokens, or internal identifiers in the query string, those then leak to third-party analytics, ad networks, or any external resource your page loads.

Safe example:

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

This is also the current browser default — setting it explicitly makes that behaviour intentional and consistent across clients and frameworks rather than left to chance. It sends the full path only within your own site, the bare origin (scheme and host, no path or query) to other sites, and nothing at all when downgrading from HTTPS to HTTP.

The gotcha: the danger is loosening it, not leaving it. The old default no-referrer-when-downgrade still exposes full URLs to any HTTPS third party, and unsafe-url sends them everywhere — both undo the protection. The tightest option, no-referrer, can break legitimate flows that rely on the referrer (some payment gateways and analytics). Whatever policy you choose, the deeper fix is to never put secrets in URLs in the first place.

Permissions-Policy

Mitigates: abuse of powerful browser features. This header lets you explicitly disable APIs your site does not use — camera, microphone, geolocation, USB, payment — so that neither your own page nor any embedded third-party content can silently invoke them. It shrinks the attack surface available to injected or compromised scripts.

Safe example: deny everything you do not need.

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

An empty allow-list () means "no origin may use this feature." Grant a feature to your own origin with geolocation=(self) only where you genuinely need it.

The gotcha: this header replaced the older Feature-Policy, and the syntax is different — copying an old Feature-Policy string verbatim will not work. Adopt a deny-by-default posture and open up features one at a time, rather than listing a few to block and leaving the rest open.

What it does not do: it governs browser feature access, not data handling. It will not stop a script you have deliberately loaded from doing whatever those granted features allow.

A quick reference

Header Primary attack it mitigates
Content-Security-PolicyXSS / content injection impact
Strict-Transport-SecurityProtocol downgrade / SSL stripping
X-Frame-OptionsClickjacking
X-Content-Type-OptionsMIME sniffing
Referrer-PolicyReferrer / token leakage
Permissions-PolicyAbuse of browser feature APIs

Two things worth stressing before you deploy. First, headers interact: your CSP's frame-ancestors and X-Frame-Options should agree, and an over-broad CORS policy can undermine the isolation these headers assume — see CORS misconfiguration. Second, none of these is a substitute for fixing the underlying bug. They are the layer that limits how much a mistake elsewhere can cost you.

How Cyber Report checks this

An external Cyber Report scan reads your live response headers without touching your server's internals: it flags missing Strict-Transport-Security, Content-Security-Policy, X-Frame-Options, X-Content-Type-Options and Referrer-Policy, grades the quality of any CSP you do have — catching 'unsafe-inline', 'unsafe-eval', wildcard and plain-http: sources, and a missing object-src or frame-ancestors — and checks whether your HSTS configuration is actually preload-list eligible. It also notes server version details leaked in Server and X-Powered-By headers.

See what we scan, or run a scan on your own domain to see where you stand.