What you'll learn
- Quick answer
- The Headers That Don't Get the CSP Spotlight
- Strict-Transport-Security: Killing the HTTP Downgrade Window
- X-Frame-Options and frame-ancestors: Stopping Clickjacking
- X-Content-Type-Options: Stopping MIME Sniffing
- Referrer-Policy: Deciding What Leaks in the Referer Header
- Permissions-Policy: Locking Down Camera, Mic, and Friends
- Setting All of Them at Once and Verifying They Landed
- FAQ
Quick Answer
Security headers are HTTP response headers that tell the browser how to treat your site defensively — force HTTPS on every future visit, block your pages from being framed by another site, stop the browser from guessing file types, control what leaks in the Referer header, and restrict which browser features a page can use. None of them need application logic changes; they need to be set correctly, consistently, on every response, which is where most sites fall short.
The Headers That Don't Get the CSP Spotlight
If you've read our dedicated post on Content Security Policy, you know CSP is the heavyweight of this group — a single header that can block XSS outright by controlling what scripts, styles, and resources a page is allowed to load. This post is deliberately not about CSP. It's about the five other response headers that sit alongside it, each closing a different, narrower gap that CSP doesn't touch.
None of these require a framework, a library, or backend code changes. They're response headers — set them once at your web server, reverse proxy, or CDN, and every response carries them. The catch is that "just a header" is easy to get syntactically wrong in ways that either do nothing or quietly break your own site, so we've verified every example below against the actual specification rather than reciting it from memory.
Strict-Transport-Security: Killing the HTTP Downgrade Window
Even a site that serves everything over HTTPS has a gap: the first time a browser resolves your domain, or any time a user types http:// or clicks an old plain-HTTP link, the very first request can go out unencrypted before a redirect to HTTPS ever happens. An attacker sitting on that network path — a coffee shop Wi-Fi, a compromised router — can intercept that first plaintext request and never let the redirect through. HSTS (HTTP Strict Transport Security) closes that gap by telling the browser, after one successful HTTPS visit, to never attempt HTTP for this domain again, for a set period of time, no matter what the user types or clicks.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preloadmax-age is in seconds — 63072000 is two years, and browsers commonly recommend at least one year (31536000) before a domain qualifies for preload. includeSubDomains extends the policy to every subdomain of your domain, which is powerful but dangerous if any subdomain still serves plain HTTP; turning this on before every subdomain is HTTPS-ready will break those subdomains outright. preload is a request to be baked directly into the HSTS preload list shipped inside Chrome, Firefox, and Edge themselves, so even a user's very first-ever visit is protected, before the browser has seen your header even once.
Preload is close to permanent in practice: getting removed from the list, once you're baked into browser binaries, takes months to propagate as those browser versions roll out to users. The sane rollout order is to ship a short max-age first, confirm nothing broke, raise it in stages, add includeSubDomains once every subdomain is verified HTTPS-only, and only then submit to hstspreload.org.
X-Frame-Options and frame-ancestors: Stopping Clickjacking
Clickjacking loads your page inside an invisible or disguised iframe on an attacker's site, then tricks a user into clicking something that looks harmless but is actually a button on your page underneath — a "claim your prize" button that's really your bank's "transfer funds" button, positioned pixel-for-pixel under the attacker's fake UI. X-Frame-Options stops this by telling the browser whether your page is allowed to be framed at all.
X-Frame-Options: SAMEORIGINOnly two values are meaningful today: DENY blocks framing entirely, even by your own site, and SAMEORIGIN allows framing only by pages on your own origin. A third value, ALLOW-FROM, is deprecated and unsupported in modern browsers — if you need to allow specific other origins to frame your page, that job belongs to CSP's frame-ancestors directive instead, which supports a real list of allowed origins:
Content-Security-Policy: frame-ancestors 'self' https://partner.example.comframe-ancestors is the modern, more capable mechanism and takes precedence in any browser that supports CSP — but it's worth sending X-Frame-Options alongside it anyway, as a one-line, zero-cost fallback for any client that only understands the older header.
X-Content-Type-Options: Stopping MIME Sniffing
Older browsers used to "sniff" the actual bytes of a response to guess its content type, even when the server's Content-Type header said something else. That behavior existed for compatibility with misconfigured servers, but it opens a real hole: if an attacker can get a file with embedded script content hosted on your domain — a user avatar upload, say — a browser that sniffs might decide to execute it as HTML or JavaScript instead of treating it as the harmless image type your server declared.
X-Content-Type-Options: nosniffThere is exactly one valid value, nosniff, and it does exactly one thing: it tells the browser to trust the declared Content-Type and never override it by guessing. There's no configuration decision to make here and no legitimate reason to omit it — it's one line with no downside.
Referrer-Policy: Deciding What Leaks in the Referer Header
Whenever a browser navigates away from your page, or loads a resource from another origin, it can send a Referer header revealing the URL the user came from — including query strings. If a password reset link, a search query, or an internal document ID lives in that URL, it can leak to every third-party script, ad, or embedded resource on the next page. Referrer-Policy controls exactly how much of that URL gets sent.
Referrer-Policy: strict-origin-when-cross-originThe full set of valid values is no-referrer, no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, and unsafe-url. strict-origin-when-cross-origin is the modern browser default and a sane one for most sites: it sends the full URL to same-origin requests, only the bare origin (no path or query) to cross-origin requests, and nothing at all when downgrading from HTTPS to HTTP. If your URLs ever carry sensitive query parameters, tighten this further to same-origin or no-referrer.
Permissions-Policy: Locking Down Camera, Mic, and Friends
Permissions-Policy (the successor to the older, experimental Feature-Policy header) restricts which browser APIs your page — and any iframe embedded inside it — is allowed to invoke at all: camera, microphone, geolocation, fullscreen, payment, and dozens more. Even if your own code never calls navigator.geolocation, a compromised third-party script or ad tag on your page could try to, and this header shuts that door regardless of what the script attempts.
Permissions-Policy: geolocation=(self "https://maps.example.com"), camera=(), microphone=(), payment=()The syntax is genuinely different from every other header here: each feature is followed by an allowlist in parentheses. An empty allowlist, (), disables the feature for everyone, including your own top-level page. self permits only your own origin. A quoted origin adds a specific third party — useful if, say, an embedded maps widget genuinely needs geolocation. * allows every origin, which is rarely what you want. A marketing or content site with no legitimate use for any of these APIs can usually disable almost everything by default and allowlist only what it actually uses.
Setting All of Them at Once and Verifying They Landed
A reasonable combined baseline for a typical content site looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=()To check what's actually being sent, run curl -I https://yoursite.com and read the response headers directly — that's ground truth, unaffected by any caching layer's dashboard lying to you. A quick online scanner is useful too, but treat curl as the source of truth since it shows exactly what a real client receives.
Setting these at the CDN or reverse-proxy layer — Cloudflare Transform Rules, an Nginx add_header directive, or a hosting platform's headers() config — is usually more reliable than adding them per-route in application code, because it's applied to every response by default instead of something you have to remember to add every time you create a new route.
