Security headers are one of the highest-leverage things you can ship: a handful of response headers, no application logic changes, and you close off entire classes of browser-side attacks. They’re also one of the most commonly half-implemented — a Content-Security-Policy copy-pasted from a blog post that doesn’t match your actual asset origins, or an HSTS header set without includeSubDomains on a site that has subdomains.
Here’s what actually matters, in rough order of impact.
Content-Security-Policy (CSP)
CSP tells the browser which sources of scripts, styles, images, and other resources are allowed to load. Done well, it’s your strongest defense against XSS — even if an attacker injects a <script> tag, the browser refuses to execute it if the source isn’t allowlisted.
A reasonable starting policy:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'
Where teams get this wrong:
- Blanket
'unsafe-inline'onscript-srcto avoid dealing with inline scripts — this defeats most of CSP’s XSS protection. Use nonces or hashes instead. - Forgetting third-party origins — analytics, font CDNs, payment widgets — then either breaking the site or loosening the policy with a wildcard to make errors go away.
- Never actually testing it — CSP violations show up silently in the browser console unless you’re watching for them. Ship it in
Content-Security-Policy-Report-Onlyfirst if you’re not confident.
Building a correct policy by hand for a real app with multiple script/style/font origins gets tedious fast. Our CSP Builder lets you compose a policy directive-by-directive and see the resulting header string, so you’re not hand-assembling semicolon-separated strings and hoping you didn’t typo a directive name.
Strict-Transport-Security (HSTS)
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
HSTS tells browsers to never attempt an HTTP connection to your domain again, even if a user types http:// or clicks an old link — closing the window for SSL-stripping attacks. The two mistakes: setting a short max-age (defeats the purpose — use at least a year), and forgetting includeSubDomains on a domain that has subdomains you didn’t think to check.
X-Frame-Options / frame-ancestors
Prevents your pages from being embedded in an <iframe> on someone else’s site — the core defense against clickjacking. frame-ancestors 'none' in your CSP supersedes the older X-Frame-Options header in modern browsers, but setting both is still standard practice for older browser compatibility.
X-Content-Type-Options
X-Content-Type-Options: nosniff
One line, prevents browsers from MIME-sniffing a response into a different content type than declared — closes off a class of attacks where a file uploaded as an “image” gets executed as a script because the browser guessed the type.
Referrer-Policy
Referrer-Policy: strict-origin-when-cross-origin
Controls how much of your URL leaks to third-party sites via the Referer header when a user clicks an outbound link. If your URLs ever contain sensitive query parameters (tokens, IDs), the default browser behavior leaks more than you’d want.
Permissions-Policy
Explicitly disables browser features (camera, microphone, geolocation, USB, etc.) your site doesn’t use — reduces the attack surface available to any script that does get injected, and to third-party iframes you embed.
Checking what you actually have
The gap between “we set security headers” and “our headers are correct” is usually invisible until someone audits it. Our Security Headers Auditor checks a live URL against this exact checklist and flags what’s missing or misconfigured — a fast way to verify a deploy actually shipped what you intended, not just what you wrote in the config.
Two adjacent tools worth having in the same workflow:
- CORS Builder — generate a correct
Access-Control-*header set instead of hand-writing one and discovering the gaps in production. - SRI Hash Generator — generate Subresource Integrity hashes for any third-party scripts or stylesheets you load, so a compromised CDN can’t silently serve modified code to your users.
The takeaway
None of these headers require an architecture change — they’re response headers, set once, that meaningfully raise the cost of a wide range of client-side attacks. The failure mode isn’t ignorance, it’s drift: a policy that was correct at launch and never revisited as new script origins, subdomains, or embedded widgets got added. Worth a five-minute audit any time you ship something new that changes what your site loads.