Security Education
8 min
If you’ve read my 30 minute security checklist, you’ll have seen security headers come up as the very first step, with a two minute check via securityheaders.com. What that post doesn’t do is explain what security headers actually are, why each one matters, and what the real world consequences of missing them look like. This post is that explanation.
That said, a 2025 scan of the top one million websites found that fewer than 25% had deployed a meaningful Content Security Policy, and nearly 40% were still missing basic protections like X-Content-Type-Options. So this is not an obscure problem, it’s an extremely common one, and it’s worth understanding properly.
What security headers actually are
Every time someone visits your app, your server sends back a response. That response contains two parts: the actual content of the page, and a set of headers that travel alongside it. Most of those headers are fairly mundane, things like content type and cache instructions. Security headers are a specific subset of those that exist purely to tell the browser how to behave from a security standpoint.
So think of them as guardrails that sit between your app and the browser. Instructions like “don’t load scripts from anywhere except this domain”, “don’t let this page be embedded in an iframe”, “only communicate over HTTPS”. The browser reads these and enforces them before any application code even runs.
They cost essentially nothing to implement. They require no changes to your application logic. And they eliminate entire classes of attack at the browser level rather than requiring you to patch them individually in your code. The reason they get missed so consistently is simply that AI tools don’t configure them by default, and most founders don’t know to look for them.
The four that matter most
There are more than four security headers worth knowing about, but these are the ones that come up most consistently in my own audits and the ones with the most direct consequences when missing.
Content Security Policy (CSP)
CSP is the most powerful of the bunch and also the most complex to configure. In plain terms, it lets you define an allowlist of sources that the browser is permitted to load scripts, styles, images, and other resources from. Anything not on the list gets blocked outright.
The main thing CSP protects against is cross site scripting (XSS), which is where an attacker manages to inject malicious script into your page. Without CSP, that script runs. With a properly configured CSP that excludes ‘unsafe-inline’, injected scripts simply don’t execute, even if the injection itself happens.
The most common misconfiguration worth knowing about: CSP policies that include ‘unsafe-inline’ in the script-src directive provide almost no XSS protection whatsoever. It’s the equivalent of fitting a lock on a door and then leaving it permanently open because locking it was inconvenient. If your CSP has ‘unsafe-inline’ in it, it’s effectively doing very little.
The practical approach for adding CSP without breaking your app is to start with Content-Security-Policy-Report-Only, which logs violations without enforcing them, run it for a couple of weeks, audit what it surfaces, and then move to enforcement once you’ve built an accurate allowlist from real traffic.
HTTP Strict Transport Security (HSTS)
HSTS instructs the browser to refuse all plain HTTP connections to your domain and communicate exclusively over HTTPS for a specified duration. Without it, an attacker on the same network can strip HTTPS from a connection using a fairly basic tool called SSLstrip, forcing the browser to communicate over unencrypted HTTP without any visible warning to the user.
One thing worth knowing before you implement it: HSTS with includeSubDomains means every current and future subdomain on your domain also needs to serve valid HTTPS. If you have any subdomains that don’t support HTTPS (staging environments, old campaign pages, anything at all), adding includeSubDomains will make them unreachable for however long your max-age is set. Worth auditing your subdomains before committing to it.
The preload option takes this further by getting your domain baked into the browser binary itself, so that even the very first connection is forced to HTTPS. Once you’re on the preload list, removal takes weeks to months as the change propagates through browser releases, so it’s a commitment worth making deliberately rather than casually.
X-Frame-Options
This one controls whether your app can be embedded in an iframe on another site. Without it, a malicious site can load your app invisibly inside their own page and overlay a transparent interface on top of it, tricking users into clicking things they can’t see. This is called clickjacking, and it requires essentially zero technical skill to pull off.
Worth noting: the modern standard here is actually to use the frame-ancestors directive inside your CSP rather than X-Frame-Options separately, since it’s more expressive and supported by all modern browsers. That said, setting both is harmless and covers older browsers, so it’s not a case of choosing one or the other if you want maximum coverage.
Permissions-Policy
This one controls which browser features your app is allowed to access, things like the camera, microphone, geolocation, and payment APIs. The reason it matters is that any JavaScript running on your page (including third party scripts, compromised dependencies, and ads) can request access to these APIs unless you explicitly restrict them. The CrowdStrike 2025 Global Threat Report documented a 200% increase in attacks targeting browser-accessible device APIs through compromised third party scripts specifically.
From my own audits, Permissions-Policy is the most consistently absent header across every stack I look at. It shows up less frequently than HSTS, less frequently than CSP, and less frequently than X-Frame-Options. Most apps genuinely don’t need camera, microphone, or geolocation access, and yet they leave all of those APIs completely unrestricted by default.
One thing to be aware of on the implementation side: the old syntax used Feature-Policy with geolocation ‘none’, but Permissions-Policy uses geolocation=(). Mixing formats silently fails, meaning the header appears to be set but does nothing. Worth double checking which syntax your setup is using.
How to check what you currently have
The fastest way is securityheaders.com. Paste your live domain, run the check, and look for a grade of B or above. The report breaks down exactly which headers are present, which are missing, and flags common misconfigurations. Takes about ten seconds and requires zero technical knowledge to interpret.
Mozilla Observatory is worth running alongside it as a second opinion. Both are free, both give you actionable output, and between the two of them you’ll have a very complete picture of where you stand.
The fix, generally speaking
For most stacks, HSTS, X-Frame-Options, X-Content-Type-Options, and a basic Permissions-Policy can be added in a single config change and are unlikely to break anything. CSP is the one that requires more care, and the Report-Only approach above is the right way to go about it without risking functionality.
If you’re on Express/Node.js, the helmet middleware package applies sensible defaults for all of these in a single line. There’s really no reason to configure each header manually from scratch in 2026 if you’re on that stack.
One thing to verify after deploying: if you’re behind a CDN or reverse proxy, confirm that the headers are actually making it through to the end user. Some CDNs strip or override headers at the edge, meaning your origin is sending them correctly but the user never sees them. Checking the actual response headers in DevTools after deployment rather than just assuming it worked is worth the extra two minutes.
Back to blog
