Most websites fail their first security scan not because of exotic zero-days, but because of a handful of missing HTTP response headers. The good news: security headers are among the cheapest fixes in all of web security. The bad news: they are also the most commonly misunderstood — teams copy a Content-Security-Policy from a blog post, break their own JavaScript, rip it out again, and leave the header off "for now". That "for now" regularly turns into years.
This guide walks through every security header that matters in 2026, what an attacker can actually do when it is missing, and how to deploy each one without breaking your site. Everything described here is checked automatically by the free security scan — run it on your domain, then come back to the relevant section to fix what it flags.
Table of contents
- Why headers matter more than you think
- Content-Security-Policy (CSP)
- Strict-Transport-Security (HSTS)
- X-Frame-Options and frame-ancestors
- X-Content-Type-Options
- Referrer-Policy
- Permissions-Policy
- A rollout plan that won't break production
- Frequently asked questions
Why headers matter more than you think
HTTP response headers are instructions your server sends with every response. Some are cosmetic (Server: nginx), some are functional (Content-Type), and a small, well-defined set are security controls. Unlike application code, headers apply to every response uniformly — which makes them the only defense that still works on the endpoints you forgot to protect.
Three properties make them uniquely valuable:
- Blanket coverage. One nginx line protects every route, including the admin panel someone added last week.
- Browser-enforced. The control executes in the victim's browser, exactly where XSS and clickjacking attacks run. No server-side patching race.
- Free. No libraries, no refactors, no performance cost worth measuring.
The counter-argument you'll hear is "headers are just checkboxes". That was defensible in 2015. Today, Content-Security-Policy in enforcement mode is a genuine exploit-mitigation layer, and Strict-Transport-Security is the difference between a network attacker downgrading your users to plain HTTP or not. Google's own research has repeatedly identified CSP as one of the highest-leverage XSS mitigations available.
Content-Security-Policy (CSP)
What it does
CSP is a browser-enforced allowlist for everything a page is allowed to load and execute: scripts, styles, frames, connections, images, fonts. A policy like:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; object-src 'none'; frame-ancestors 'none'; base-uri 'self'
tells the browser: only load resources from this site and cdn.example.com, never run inline <script>, never load plugins, never allow framing. If an attacker manages to inject <script>alert(document.cookie)</script> into your page — the classic XSS payload — the browser refuses to execute it, because the injected script does not come from an allowed source.
What breaks without it
An XSS bug in your app goes from annoying-but-blocked to full account takeover. With no CSP, every injected script runs: keyloggers, session-cookie exfiltration, crypto-mining, fake login forms rendered on top of your real page. CSP does not fix the bug — you still must — but it converts whole classes of injection from "catastrophe" into "visible console error".
How to deploy it without breaking everything
The mistake almost everyone makes is starting with a strict policy. Start instead with report-only mode, which tells the browser to evaluate the policy and report violations without blocking anything:
# nginx — phase 1: observe
add_header Content-Security-Policy-Report-Only "default-src 'self'" always;
Collect violations for one to two weeks (browser console, or a reporting endpoint via report-uri). What you will almost certainly discover: inline <script> blocks, eval() in an old library, and a CDN you forgot about. Then modernize in this order:
- Eliminate inline scripts. Move each
<script>code</script>into an external file, or if you truly cannot, add its SHA-256 hash toscript-src:
Hashes are precise: only that exact script content runs. Avoidscript-src 'self' 'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=''unsafe-inline'— it silently disables most of CSP's protection. - Replace
eval()-style loaders. Libraries callingeval,new Function, orsetTimeout("string")need upgrading or replacing; the corresponding policy keyword is'unsafe-eval', and shipping it guts the policy. - Lock down in stages:
default-src 'self'is your base; allow only what the violation reports prove you need. Finish withobject-src 'none',base-uri 'self', andframe-ancestors 'none'(orframe-ancestors 'self'if you embed your own pages). - Flip to enforcement once reports are quiet:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com; object-src 'none'; frame-ancestors 'none'; base-uri 'self'" always;
The always modifier matters: without it, nginx adds the header only to responses with certain status codes, and error pages (a classic injection target) go unprotected.
Common pitfalls
'unsafe-inline'+default-src: any policy containing'unsafe-inline'inscript-srcis largely decorative. An attacker's inline payload runs.*in any directive: same effect — the directive no longer constrains anything.- Forgetting subdomains:
script-src 'self'does not coverhttps://static.example.com; spell out scheme + host. - Frame-based OAuth flows:
frame-ancestors 'none'can break flows where a provider frames your callback page. Test logins before shipping.
Strict-Transport-Security (HSTS)
What it does
HSTS is a one-line header that tells browsers: for the next N seconds, talk to this domain over HTTPS only — never attempt plain HTTP, and never accept a bad certificate.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
What breaks without it
TLS termination at the server does not protect the first connection a user makes. On a hostile network — café Wi-Fi, an office firewall, a compromised router — an attacker can intercept the initial plain-HTTP request and answer it themselves (SSL-stripping). The user sees your site; every keystroke flows through the attacker. HSTS closes this window because after the first secure visit the browser refuses to speak HTTP to your domain at all.
How to deploy it
Start conservatively and grow the max-age — the header commits browsers for its whole duration, and a mistake is expensive to walk back:
Strict-Transport-Security: max-age=300for a few days. Watch for mixed-content breakage on subresources.- Raise to
max-age=2592000(30 days), thenmax-age=31536000(a year) once nothing screams. - Only when every subdomain is HTTPS-clean, add
includeSubDomains. - Only when you are certain every subdomain — including
dev.and internal names that resolve publicly — serves valid HTTPS, submit to the hstspreload.org list and addpreload. Preload is effectively irreversible on human timescales; browsers ship the list baked in.
Also serve the header on plain-HTTP responses (per RFC 6797 the header is ignored there, but many proxies only strip correctly when instructed) and — critically — on every HTTPS response, including redirects and error pages. An HSTS header on / alone leaves the rest of the domain unprotected.
Finally: HSTS does not make http:// links work. It makes browsers upgrade them before the request leaves the machine. Certificate problems that used to show a bypass warning now show a hard, unbypassable error — which is why the gradual max-age ramp exists.
X-Frame-Options and frame-ancestors
What it does
Two headers, one job: stop other sites from embedding your pages in an <iframe>.
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
DENY forbids framing entirely; SAMEORIGIN allows only your own origin to frame you.
What breaks without it: clickjacking
An attacker buys an ad-cloaked domain, frames your "Delete account" or "Transfer funds" page, and sets the iframe to near-zero opacity. On top of it they stack a decoy UI — a fake video player with a big play button positioned exactly over your real "Confirm" button. The victim thinks they are clicking "Play"; the click lands on your page, in the attacker's framing context, with the victim's cookies attached. That is clickjacking, and it works against any state-changing page that lacks framing protection.
How to deploy it
Modern practice is to do both:
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none'" always;
frame-ancestors is the modern control (part of CSP, supported everywhere that matters); X-Frame-Options remains for very old browsers. If parts of your product are legitimately embedded by customers (widgets, embeddable dashboards), use SAMEORIGIN for the app and exempt only the embeddable routes with a deliberate Content-Security-Policy: frame-ancestors https://partner.example.com.
X-Content-Type-Options
X-Content-Type-Options: nosniff
Historically, browsers "sniffed" response bodies when the declared Content-Type seemed wrong. Upload a .txt that contains HTML, and some browsers would happily render it as HTML — on your origin, inheriting your cookies and scripts. User-uploaded content served from your domain becomes stored XSS with no injection bug in your app at all.
nosniff disables guessing: the browser trusts the declared type or refuses to render. With it set, a script response without Content-Type: text/javascript is blocked outright, which also catches server misconfigurations early. One line, zero breakage risk on correctly-typed responses, meaningful protection on user-content paths. Set it and forget it.
Referrer-Policy
What it does
Controls how much of your users' URLs leak to third parties via the Referer header as they click outbound links.
Referrer-Policy: strict-origin-when-cross-origin
What breaks without it
The default in the absence of a policy still sends full URLs in many navigation cases. Your /account/reset?token=..., /search?q=..., and /invoice/8817 URLs can end up in the analytics and server logs of every site your users click away to. Token-bearing URLs in referrer logs are a recurring, real-world account-takeover source — the logs of some random SaaS are a much quieter place to hunt than your own.
How to deploy it
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
This keeps your origin visible to partners (useful for analytics attribution) but strips paths and query strings on cross-origin navigation. If you never need cross-origin referrers at all, no-referrer is stricter and equally safe.
Permissions-Policy
What it does
The newest header of the set: a deny-by-default capability switchboard for powerful browser features — camera, microphone, geolocation, USB, payment APIs and dozens more.
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Empty parentheses mean "granted to no origin, including your own". The threat is iframe inheritance and malicious third-party scripts: a compromised ad script inside your page can otherwise silently invoke navigator.mediaDevices.getUserMedia() in contexts where the browser still trusts the top-level origin. Deny everything you do not consciously use; grant specific features to specific origins only when a product feature demands it.
A rollout plan that won't break production
Deploying headers site-wide is a config change; a mistake affects 100% of users instantly. Order of operations that has worked repeatedly:
- Inventory. Scan your domain (the free scan reports the full header picture in one pass) and note what is present, missing, and what third-party scripts/styles/fonts your pages actually load.
- Stage 1 — the free wins.
X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin,X-Frame-Options: DENY,Permissions-Policydeny-all. These essentially never break anything. Ship them the same day. - Stage 2 — HSTS ramp.
max-age=300→ 30 days → 1 year, watching mixed-content warnings between steps.includeSubDomainsandpreloadonly after a full subdomain audit. - Stage 3 — CSP in report-only for two weeks, fix violations, then enforce.
- Verify continuously. Headers drift: a new microservice forgets the config, a CDN strip re-appears. Re-scan after every deploy of new infrastructure, and add the scan to your release checklist.
Frequently asked questions
Do security headers slow my site down?
No — they add a few hundred bytes to responses. There is no measurable rendering or network performance cost.
Do headers replace fixing XSS bugs?
No. CSP is a mitigation layer, not a fix. Attackers defeat misconfigured policies with ease; treat headers as the safety net under correct application code, not the net's replacement.
Which header should I ship first?
X-Content-Type-Options: nosniff and Referrer-Policy — zero risk, immediate value — then HSTS via the staged ramp, then CSP last, because it needs the violation-reporting cycle.
Does HSTS protect my users on their very first visit?
No. The very first plaintext request can still be intercepted; only after one secure response does the browser pin the domain. That residual gap is exactly what the preload list exists to close.
Why does my CSP break only on production?
Because report-only violations don't block anything, a policy that "works" locally may be silently violated in production by injected third-party tags, A/B-test snippets, or error-tracking beacons. Read the console violations, then extend the policy for those specific sources.
Run the free security scan to see which of these headers your site is missing — the scan checks all of them in one pass, alongside TLS, cookies, email security and fifteen other checks.
Remote Daemon