The Headers Browsers Ask You For (And What Happens When You Skip Them)
Every HTTP response your server sends carries instructions for the browser. Most of those instructions are about content — what type, how big, how long to cache. A small, specific set is about security: rules that tell the browser to refuse certain attacks against your users even when an attacker has tricked them into clicking a link or loading a page.
Missing security headers do not break your site. Lighthouse will not fail. Your monitoring will not page. The cost shows up only when something goes wrong — a stolen cookie, a clickjacked transaction, a downgraded HTTPS connection, an XSS payload that runs because the browser had no policy saying it should not. Security headers are the cheapest defense in depth you have, and most sites ship without them because nothing forces the issue.
This guide covers the six HTTP response headers every public site should set: Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, and Content-Security-Policy. For each one you get what it does, what attack it stops, a copy-pasteable value, and the misconfigurations that quietly defeat the protection. A seven-item checklist at the end gives you the order to roll them out without breaking your site.
Strict-Transport-Security (HSTS): Lock the Browser to HTTPS
What it does: HSTS tells the browser to never connect to your domain over plain HTTP again, for a duration you choose. Once a browser has seen this header on an HTTPS response, it will silently rewrite every future http:// request for that hostname to https:// — without even sending the original request over the wire.
What attack it stops: Stripping. Without HSTS, an attacker on the same network (coffee shop Wi-Fi, malicious access point, compromised router) can intercept the first plaintext request a user makes to your site and serve a fake HTTP version. The browser never gets a chance to upgrade to HTTPS because it never sees your real server's redirect. With HSTS already cached, that attack window closes entirely.
How to set it up: Send this header on every HTTPS response:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
This says: remember for two years, apply to every subdomain, and I want to be on the browser's hardcoded preload list. After running this in production for a few weeks with no issues, submit your domain at hstspreload.org to skip even the first request.
Common mistakes to avoid:
- Setting a short max-age "just in case." A max-age of 300 (five minutes) gives an attacker a five-minute window every time a user visits to strip HTTPS. Pick a real duration — six months minimum, two years for preload eligibility.
- Sending HSTS over HTTP. Browsers ignore the header on HTTP responses. It must come over HTTPS to count. If your HTTP-to-HTTPS redirect strips the header, the redirect target still needs to set it.
- Adding
includeSubDomainsbefore checking your subdomains. If any subdomain (a status page, a legacy admin panel, an internal tool exposed by accident) is served over HTTP, that subdomain becomes unreachable to anyone whose browser has cached the HSTS policy. Audit subdomains first, then add the directive. - Preloading too early. Once your domain is on the preload list, removing it takes weeks. Run with a long max-age and
includeSubDomainsin production for at least a month before submitting.
X-Content-Type-Options: Stop Browsers From Guessing
What it does: The header has exactly one valid value — nosniff — and it tells the browser to trust the Content-Type you sent, instead of trying to guess based on the file's contents. No more "this looks like JavaScript even though it was served as text/plain, let me execute it just in case."
What attack it stops: MIME confusion attacks. If your site accepts user uploads (avatars, attachments, profile media) and serves them back from your own origin, an attacker can upload a file containing valid HTML or JavaScript but mislabel its extension. Without nosniff, the browser may sniff the content and execute it in your origin's security context — same-origin to your cookies, your CSRF tokens, everything.
How to set it up:
X-Content-Type-Options: nosniff
That is the entire configuration. Set it on every response. There is no downside, no compatibility issue, no edge case that justifies leaving it off.
Common mistakes to avoid:
- Setting it only on HTML responses. The attack surface is uploads and downloads, not your pages. Set it globally so static assets, API responses, and any user-served content all carry it.
- Assuming a CDN sets it for you. Some do, most do not. Check actual response headers with
curl -Ifrom a real client, not the dashboard. - Trusting your
Content-Typeto be correct in the first place. If your upload endpoint serves everything asapplication/octet-stream,nosniffprotects you. If it lets users control theContent-Type, you have a bigger problem than headers.
X-Frame-Options: Refuse to Be Embedded
What it does: Tells the browser whether your pages are allowed to be rendered inside a <frame>, <iframe>, or <object> on someone else's site. The modern replacement is the frame-ancestors directive in CSP, but X-Frame-Options is still respected everywhere and is the right header to set today.
What attack it stops: Clickjacking. An attacker loads your real, logged-in app inside a hidden iframe on their site and overlays it with bait — a fake "claim your prize" button positioned exactly over your "transfer funds" button. The user clicks the bait, the click registers on your real page, action completes. Without X-Frame-Options, browsers happily embed any page.
How to set it up:
X-Frame-Options: DENY
Use DENY unless you legitimately need your own pages to embed each other, in which case use SAMEORIGIN. The older ALLOW-FROM directive is deprecated and ignored by Chrome and Safari — use CSP's frame-ancestors if you need to allow specific external origins.
Common mistakes to avoid:
- Skipping it because "we have CSP frame-ancestors." You should have both. X-Frame-Options covers older browsers and clients (some embedded webviews, older Edge versions) that do not honor CSP's frame-ancestors. They cost nothing together.
- Using
SAMEORIGINwhen you meanDENY. If no part of your app embeds your own pages in iframes, useDENY. SAMEORIGIN is a slightly larger attack surface for no benefit. - Setting it only on the login page. Clickjacking targets any authenticated action — transfer money, change email, delete account. Apply it globally.
Referrer-Policy: Stop Leaking URLs to Third Parties
What it does: Controls what the browser puts in the Referer header when your users click a link to another site or load a third-party resource. The default behavior varies by browser, but historically browsers sent the full URL — including paths and query strings — to every external destination.
What attack it stops: Data leakage. If your URLs contain session tokens, password reset hashes, internal identifiers, or anything sensitive in the path or query string, every outbound link, every embedded image from a CDN, every analytics pixel ships that data to a third party. There are documented cases of password reset links and OAuth state parameters showing up in third-party server logs because of permissive referrer policies.
How to set it up:
Referrer-Policy: strict-origin-when-cross-origin
This sends the full URL on same-origin requests (so your own analytics still work), only the origin on cross-origin HTTPS requests, and nothing at all on HTTPS-to-HTTP transitions. It is also the modern browser default — setting it explicitly makes the behavior predictable regardless of client.
Common mistakes to avoid:
- Setting
no-referrer-when-downgrade"for compatibility." That policy sends the full path to every cross-origin HTTPS destination. It was the old browser default and is the most permissive realistic setting. - Putting sensitive tokens in URLs and relying on Referrer-Policy to save you. The header is defense in depth. Sensitive tokens should be POST bodies or headers, not query strings, regardless of policy.
- Using
no-referrersitewide. Some legitimate integrations (affiliate tracking, federated login flows, OAuth) depend on the Referer being set on same-origin or partner-origin requests.strict-origin-when-cross-originis the sweet spot.
Permissions-Policy: Turn Off APIs You Do Not Use
What it does: Lets you declare which browser features your site (and any embedded iframes) are allowed to use — camera, microphone, geolocation, payment APIs, accelerometer, USB, serial, autoplay, fullscreen, and dozens more. Features you do not opt into are blocked, even if compromised JavaScript on your page tries to access them.
What attack it stops: Capability creep after a compromise. If an attacker manages to inject a script into your page through any means — a third-party library compromise, a stored XSS that bypassed CSP, a malicious browser extension — Permissions-Policy is the difference between "they can read the DOM" and "they can read the DOM and turn on the user's microphone."
How to set it up: Start by denying everything you do not use:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()
The empty parentheses mean "no origin is allowed to use this feature." For features you do need on your own origin, use (self). For features used by a specific embedded partner, use (self "https://partner.example.com").
Common mistakes to avoid:
- Allow-listing features you might use someday. The header is cheap to change. Set the strictest possible policy now; loosen it when an actual feature requires it.
- Forgetting third-party iframes. Embedded payment widgets, video players, and chat tools often need specific permissions. Test each integration after deploying the header; broken iframes usually mean a missing entry.
- Confusing it with Feature-Policy. Feature-Policy is the old name and the old syntax. Modern browsers want Permissions-Policy. Some servers send both for legacy support, but only Permissions-Policy needs to be correct.
Content-Security-Policy: The Hard One That Stops Real XSS
What it does: CSP tells the browser which sources of script, style, image, font, frame, and connection are allowed to load on your pages. Anything not matching the policy is refused at the browser level, before the script executes or the request leaves the user's machine.
What attack it stops: Cross-site scripting in its most damaging form. An XSS that gets a payload into your page is contained to whatever the policy allows. A strict CSP turns a stored XSS that would have stolen sessions and impersonated users into a script that loads, gets blocked by the browser, and shows up as a console error.
How to set it up: Start in report-only mode so you do not break the site:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; report-to csp-endpoint
Watch the violation reports for a week, fix legitimate breakage by adding sources or refactoring inline scripts, then switch the header name from Content-Security-Policy-Report-Only to Content-Security-Policy to enforce.
Common mistakes to avoid:
- Allowing
'unsafe-inline'or'unsafe-eval'inscript-src. Either one defeats most of CSP's XSS protection. Refactor inline event handlers and inline<script>blocks into external files, or use nonces or hashes if refactoring is genuinely impossible. - Using
*as a source. Specifically allowing every origin is no policy at all. Evenhttps:wildcard is too broad — name the CDNs and APIs you actually use. - Skipping
frame-ancestors 'none'. This is the modern replacement for X-Frame-Options. Set both during transition; CSP wins where supported. - Setting
upgrade-insecure-requestsas a substitute for HSTS. Upgrade rewrites HTTP requests inside your page; HSTS prevents the browser from ever making them. You want both, not one or the other. - Jumping straight to enforcement. The report-only mode exists for a reason. Every site has at least one inline script or third-party widget that breaks under a strict CSP. Find them before users do.
The Right Order to Roll These Out
Five of these six headers ship with zero risk of breaking anything: X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, and Strict-Transport-Security (with a short max-age for the first week). Set them all today. The only one requiring care is Content-Security-Policy, because a strict CSP will surface every inline script, every untracked CDN, and every third-party widget your site quietly loads — that surfacing is the point, but it needs to happen in report-only mode first.
FortWatch's headers scanner checks all six on every scan of your domains, flags missing or weak values, and tells you which of your assets are downstream of a CDN where the headers can be set in one place. The cyber hygiene scanner pairs the header audit with TLS and DNS so you see your external posture in one view rather than five tools.
What Do I Do With This?
Run through this checklist on every public hostname you own — production, staging, marketing site, status page, admin panel. Each step is testable from the command line with curl -I https://yourdomain.com.
- Set
X-Content-Type-Options: nosniffon every response. No exceptions, no edge cases. - Set
X-Frame-Options: DENY(orSAMEORIGINif you embed yourself). Apply globally, not just to authenticated pages. - Set
Referrer-Policy: strict-origin-when-cross-origin. The modern default, made explicit. - Set a strict
Permissions-Policydenying every browser API you do not use. Loosen later only when a real feature requires it. - Roll out
Strict-Transport-Securitywithmax-age=300first, raise to two years after a week, then addincludeSubDomainsafter auditing subdomains, then submit to preload. - Deploy CSP in report-only mode first. Watch violation reports, fix legitimate breakage, then switch to enforcement. Never start with
'unsafe-inline'"to make it work" — that is shipping a placebo. - Test from outside. Use
curl -Ifrom a machine that is not your office, or a free header checker. CDN caches and edge configs can silently strip headers your origin sets.
None of these headers cost anything to deploy. All of them quietly catch a class of attack that would otherwise rely on your application code being flawless. Application code is not flawless. Set the headers.
