HTTP security headers: which ones matter and what to set them to
What each header actually defends against, a starting value for each, and how to ship a CSP without taking your own site down.
What headers can and cannot do
Security headers are instructions to the browser about how to treat your site. That framing matters, because it defines the limit: they reduce the damage of a vulnerability, they do not remove it. A CSP can stop an injected script from executing; it does not stop the injection. Headers are a second line, not the first.
They are also unusually cheap. Most are one line of configuration, apply site-wide, and never change again. That combination — low cost, real mitigation — is why they are worth doing properly rather than approximately.
The short list, with starting values
| Header | Starting value | What it prevents |
|---|---|---|
| Strict-Transport-Security | max-age=31536000 | Downgrade to HTTP, and cookie theft on the first request |
| Content-Security-Policy | default-src 'self' | Injected script, and framing by other sites |
| X-Content-Type-Options | nosniff | An upload being re-interpreted as script |
| Referrer-Policy | strict-origin-when-cross-origin | Leaking full URLs — including query strings — to third parties |
| Permissions-Policy | geolocation=(), camera=(), microphone=() | Embedded content reaching for device APIs |
| X-Frame-Options | DENY | Clickjacking, for clients that ignore CSP |
Two of these deserve more than a table row.
HSTS: the one you cannot undo quickly
Strict-Transport-Security tells the browser to refuse plain HTTP for your domain for the given number of seconds. It is one of the most effective headers there is — and the only one on this list with a genuinely painful failure mode, because the browser remembers it and there is no way to reach the users who already have.
Roll it out in stages:
max-age=300— a few minutes, to confirm nothing breaks.max-age=31536000— one year, once you are confident.; includeSubDomains— only after verifying that every subdomain serves HTTPS. This is where people get caught: a legacystatus.ormail.host on plain HTTP becomes unreachable, for a year, for everyone who visited once.; preload— only when you are prepared for that commitment to be effectively permanent.
The order is the point. Each step is safe only because the previous one proved something. Skipping to the full string is how a subdomain disappears.
CSP: shipping one without breaking your site
Content-Security-Policy is the most valuable header and the most likely to break things, because it requires you to know exactly what your pages load. The way through is to let the browser tell you.
Start in report-only
Send Content-Security-Policy-Report-Only instead of Content-Security-Policy. The browser evaluates the policy exactly as it would if enforcing, logs every violation to the console, and blocks nothing. Start strict:
Content-Security-Policy-Report-Only: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
Widen only where the violations are real
Browse the site — every template, not just the homepage — and read the violations. Each one is either something you genuinely load, which you add to the policy, or something you did not know you loaded, which is worth a second look. Analytics, ad scripts, embedded fonts and third-party widgets all surface here.
Deal with inline script deliberately
Inline <script> blocks are the usual reason people give up and add 'unsafe-inline', which removes most of the benefit. The alternatives:
- Hashes. Compute the SHA-256 of each inline block and list it in
script-src. Ideal for a static site, where the set is fixed at build time and can be generated automatically. - Nonces. Emit a fresh random value per response, put it on each allowed script tag and in the policy. Necessary when the inline content varies per request.
Either way, the goal is a script-src with no 'unsafe-inline' and no 'unsafe-eval'.
Then enforce
When report-only has been quiet across the whole site for a while, change the header name. Keep the report-only version alongside if you want to test further tightening without risk — the two can be sent together.
style-src 'unsafe-inline' is a much smaller concession than the script equivalent, and is a reasonable pragmatic choice: most frameworks emit inline style attributes, and the attack value is far lower.
Permissions-Policy
Permissions-Policy (formerly Feature-Policy) disables browser features for your page and anything it embeds. An empty allowlist () means "nobody, including me":
Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=(), usb=(), interest-cohort=()
Listing features you never use costs nothing and removes them as an avenue for embedded content. If your own page needs one, use self: fullscreen=(self).
The cross-origin isolation trio
COOP, COEP and CORP are frequently recommended together by scanners, which is misleading — they solve a narrower problem (cross-origin data leaking through side channels) and COEP in particular breaks third-party embeds that do not send the matching headers.
Cross-Origin-Opener-Policy: same-origin— safe on almost any site, and worth setting. It severs the window relationship with anything you open cross-origin.Cross-Origin-Resource-Policy: same-origin— good on resources you do not want other sites hotlinking. Do not apply it blindly to assets you intend to be embeddable.Cross-Origin-Embedder-Policy: require-corp— only if you needSharedArrayBufferor precise timers. It will break ads, embedded video and most third-party widgets unless every one of them opts in.
Set COOP. Consider CORP per-resource. Leave COEP alone unless you have a specific reason.
Headers you can stop setting
X-XSS-Protection— the browser XSS auditors it controlled have been removed. Modern guidance is0, or simply not sending it; the filter itself introduced vulnerabilities.Public-Key-Pins— deprecated and removed. It caused more permanent outages than it prevented attacks.Expect-CT— obsolete; Certificate Transparency is enforced by browsers regardless.X-Powered-By,Serverversion strings — not a security control, but there is no reason to advertise your stack version. Remove them.
A scanner that still marks these missing is out of date. Which is the broader point: use a header check to find what you have forgotten, not as a score to maximise.
Frequently asked questions
Which security headers actually matter?
Content-Security-Policy (or at minimum frame-ancestors), Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy. The rest are situational. A high grade from a header scanner is not the goal — a policy that matches what your site actually does is.Will a Content-Security-Policy break my site?
Content-Security-Policy-Report-Only is for: the browser evaluates the policy and reports what would have been blocked without blocking anything. Run it in report-only until the violations stop, then flip it to enforcing.Is unsafe-inline in script-src acceptable?
Do I still need X-Frame-Options if I have CSP frame-ancestors?
frame-ancestors supersedes it in every current browser, and where both are present frame-ancestors wins. Sending X-Frame-Options: DENY as well costs nothing and covers anything that ignores CSP, so most sites keep both.Should I enable HSTS preload?
max-age and includeSubDomains for a while first.Check it on your own domain
Everything above is only useful if you can see the current state of your own setup. These tools do exactly that:
Spotted an error in this guide? Tell us at [email protected] — corrections are made and the updated date above changes with them.