Security headers are one of the cheapest hardening steps you can take — no code rewrite, no new infrastructure. Yet most sites either omit them entirely or configure them in ways that create a false sense of safety. This guide covers the mistakes we see most often in production, why they matter, and exactly how to fix them.
You installed a certificate, forced HTTPS in your app, and called it done. The problem: nothing stops a network attacker from downgrading the very first request to HTTP before your redirect fires. Without Strict-Transport-Security (HSTS), browsers have no instruction to always use HTTPS for your domain.
The fix: Add HSTS at the edge (CDN, load balancer, or web server), not only in application middleware. A solid starting policy:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Only add preload after you have confirmed every subdomain serves valid HTTPS — preload is difficult to undo. Test on staging first; a typo in HSTS can lock users out until max-age expires.
Many teams add a Content-Security-Policy header because a scanner flagged its absence, then write something like default-src * or script-src * 'unsafe-inline' 'unsafe-eval'. That satisfies the checkbox but blocks almost nothing. An attacker who injects a script tag can still run arbitrary JavaScript.
The fix: Start with default-src 'self' and open specific directives only for domains you actually use — ad networks, analytics, CDNs. Remove 'unsafe-inline' from script-src by moving inline scripts to files or using nonces. Deploy with Content-Security-Policy-Report-Only first, collect violations for a week, then enforce. Our CSP header guide walks through directive syntax, nonces, and reporting in detail.
X-Frame-Options: SAMEORIGIN prevents other sites from embedding your pages in iframes — good for clickjacking defense. But it only accepts DENY or SAMEORIGIN, cannot allow specific partner domains, and is ignored when Content-Security-Policy: frame-ancestors is present.
The fix: Prefer frame-ancestors in your CSP. Use frame-ancestors 'self' for most sites, or list trusted embedders explicitly: frame-ancestors 'self' https://partner.example.com. Keep X-Frame-Options as a fallback for older browsers, but make sure both headers agree — conflicting values confuse auditors and can leave gaps.
Browsers sometimes guess a file's MIME type when the server sends a vague Content-Type. A user-uploaded .txt file served as text/plain might be interpreted as HTML or JavaScript if it contains markup. This is MIME sniffing, and it has been exploited in real attacks.
The fix: Add X-Content-Type-Options: nosniff on every HTML response. Pair it with correct Content-Type headers on downloads and static assets. This is a one-line header with no compatibility downside on modern browsers.
Headers set in nginx and Cloudflare and your framework often produce duplicates. Some browsers merge them; others apply the most permissive value. We regularly see two Content-Security-Policy headers where one is strict and one includes 'unsafe-inline' — scanners report a pass, but enforcement is unpredictable.
The fix: Audit the full response chain. Use the Header Inspector to see exactly what browsers receive, not what you think your config file says. Remove headers at one layer only — typically the CDN or reverse proxy — and let a single source of truth own each security header.
Referrer-Policy: unsafe-url sends the full URL — including query strings with tokens, session IDs, or search terms — to every third-party resource your page loads. Analytics pixels, fonts, and ad scripts all receive it.
The fix: Use strict-origin-when-cross-origin as a sensible default. It sends the full path to same-origin requests but only the origin (no path) on cross-origin HTTPS requests, and strips the referrer entirely on downgrade to HTTP.
Modern browsers expose powerful APIs — camera, microphone, geolocation, payment, USB. If you never set Permissions-Policy (formerly Feature-Policy), any embedded iframe or compromised script can request these unless the user denies the prompt.
The fix: Disable features you do not use. A typical site needs nothing beyond maybe payment on a checkout page:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(self)
Adjust per route if needed, but default to deny.
Some CMS plugins or hosting panels inject security headers only on / or only on index.html. Subpages, API routes, and static assets in /uploads/ ship without protection. Attackers target the weakest response, not the homepage.
The fix: Apply headers globally via server or CDN config — a location / block, a Cloudflare Transform Rule, or middleware that runs on every response. Verify several URLs: homepage, a blog post, a JSON API endpoint, and a cached static file.
Run a quick scan with the Security Headers tool for a graded report, then drill into individual responses with the Header Inspector. Fix the mistakes above one at a time — HSTS and nosniff first, then tighten CSP over a reporting period. For CSP specifics, continue with our Content Security Policy guide.