Five Security Headers Worth Shipping This Week
Published
Four of these are one line each and cannot break anything. The fifth is Content Security Policy, which can absolutely break things, so it gets a rollout plan instead of a copy-paste. Plus the header everyone still ships that browsers stopped reading years ago.
Security headers are the rare kind of hardening that costs almost nothing and pays out immediately. You are not refactoring anything or adding a dependency — you are telling the browser to switch on protections it already has and is waiting for permission to use. Four of the five below are a single line each and cannot plausibly break your site. The fifth is worth doing properly, which means slowly.
1. Strict-Transport-Security
Strict-Transport-Security: max-age=31536000; includeSubDomains
HSTS tells the browser: for the next year, never talk to this host over plain HTTP, even if the user types it, even if a link says so. The browser upgrades the request internally before anything hits the network.
Why that matters more than your existing HTTP-to-HTTPS redirect: the redirect itself travels over HTTP. That first unencrypted request is the window an attacker on the same network needs. HSTS closes it, because after the first visit there is no plain HTTP request to intercept.
Three things to know before you ship it:
- Browsers ignore this header when it arrives over HTTP. It only counts on an HTTPS response.
includeSubDomainsis a real commitment. Every subdomain must serve valid HTTPS, forever, including the internal one somebody set up on plain HTTP three years ago. Audit first, then add it.- There is no undo button that works quickly. Browsers honor the
max-agethey were given. Shortening it only helps visitors who come back and receive the new value. If you want to test the waters, start atmax-age=300, confirm nothing breaks, then raise it.
There is also a preload directive, which gets your domain baked into browsers so even the very first visit is protected. It requires max-age of at least 31536000 (one year), includeSubDomains, and submission at hstspreload.org. Removal takes months to propagate. Excellent once you are certain; not a first step.
2. X-Content-Type-Options
X-Content-Type-Options: nosniff
Historically, browsers would second-guess your Content-Type header by peeking at the bytes. Serve a file as text/plain that happens to contain HTML, and the browser might helpfully decide to render it as HTML. On a site with user uploads, that is a stored XSS vector wearing a trench coat.
nosniff turns the guessing off: the declared type is the type. This is the single safest header on the list. The only way it changes behavior is if you are currently relying on a browser correcting a Content-Type you are getting wrong, in which case you have found a bug worth fixing anyway.
3. Referrer-Policy
Referrer-Policy: strict-origin-when-cross-origin
By default, clicking a link sends the full URL of the current page to the destination in the Referer header. If your URLs contain a password reset token, an internal search query, or a customer ID, you have been quietly handing that to every external site your users click through to.
This value keeps the full URL for same-origin navigation, sends only the origin cross-origin, and sends nothing when downgrading from HTTPS to HTTP. Modern browsers already use it as their default, so setting it explicitly is mostly about not depending on a default and about covering older clients. If you want to be stricter, same-origin sends no referrer off-site at all — just check with whoever reads your analytics first, because it will change what they see.
4. Permissions-Policy
Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=()
An empty parentheses list means "nobody, including this page." If your site does not need the camera, saying so means an injected script or a compromised third-party embed cannot even ask. The prompt never appears.
This one is free for most sites, because most sites use none of these. Enumerate what you actually need, disable the rest, and revisit if you ever add a feature that needs one.
5. Content-Security-Policy
CSP is the powerful one, and the one that will break your site if you paste a policy from a blog post. It is a whitelist of where content may load from, which means every resource you forgot about stops loading the moment it is enforced.
So do not enforce it first. Report first. There is a header specifically for this:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self';
object-src 'none';
base-uri 'self';
report-uri /csp-report
In report-only mode nothing is blocked. Violations get logged to the console and posted to your endpoint. Leave it running for a week or two on real traffic and the report becomes an inventory of everything your pages actually load — including the tag manager somebody added in 2022 that nobody remembers.
When you are ready to enforce, the modern approach is nonce-based rather than a list of allowed domains. Domain allowlists are brittle and easy to bypass; a nonce is a random value your server generates per response, which only your own inline scripts carry:
Content-Security-Policy:
default-src 'self';
script-src 'nonce-r4nd0mV4lu3' 'strict-dynamic' https:;
object-src 'none';
base-uri 'self';
frame-ancestors 'self'
<!-- only scripts carrying the matching nonce run -->
<script nonce="r4nd0mV4lu3">
// your inline code
</script>
How the pieces work together:
- The nonce must be freshly generated for every response and unguessable. A hardcoded nonce is decoration, not security.
'strict-dynamic'lets a script you trusted load further scripts, which is what makes real applications survive CSP without an ever-growing domain list.https:is a fallback for older browsers that do not understand'strict-dynamic'. Browsers that do understand it ignore the fallback.object-src 'none'andbase-uri 'self'close two bypasses that are easy to forget: legacy plugin embeds, and an injected<base>tag rewriting every relative URL on the page.frame-ancestors 'self'is the modern replacement forX-Frame-Options, and it controls who may put your site in an iframe.
The header to delete
X-XSS-Protection: 1; mode=block <- remove this
This controlled an XSS filter that no current major browser ships. It does nothing today, and while it was active its behavior could itself be abused in specific cases. It survives in copy-pasted config files purely through inertia. If you find it, take it out; a proper CSP is the replacement.
All of it in Apache
<IfModule mod_headers.c>
Header always set Strict-Transport-Security \
"max-age=31536000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Permissions-Policy \
"geolocation=(), camera=(), microphone=()"
Header always unset X-XSS-Protection
</IfModule>
Two details that matter. always rather than plain set means the headers are applied to error responses too — without it, your 404 and 500 pages ship unprotected. And X-Frame-Options is kept alongside CSP's frame-ancestors deliberately: the CSP directive supersedes it, but the old header still covers browsers that predate it, and it costs one line.
The <IfModule> guard is not optional on shared hosting. If mod_headers is unavailable, an unguarded Header directive is a 500 on every request. Our .htaccess generator emits this block with the guard in place so you can review it before deploying.
Check the result
curl -sI https://example.com | grep -iE \
'strict-transport|content-security|x-content-type|referrer|permissions'
Then run the domain through securityheaders.com for a second opinion. Treat the grade as a checklist rather than a score to max out — an A with a CSP that breaks checkout is worse than a B that works.
The order I would ship them
X-Content-Type-Options,Referrer-Policy,Permissions-Policy,X-Frame-Options. Ten minutes, no realistic risk.Strict-Transport-Securitywith a shortmax-age. Confirm every subdomain is HTTPS-clean, then raise it to a year and addincludeSubDomains.Content-Security-Policy-Report-Only. Collect real violations for a couple of weeks.- Enforce the CSP, nonce-based, once the reports are quiet.
Steps 1 and 2 are an afternoon and cover most of the easy wins. Step 4 is the one that actually stops an XSS payload, and it is worth the patience.