Security Headers Every Site Needs
Security headers are the best bang for your buck in web security. A few lines in your server config or a couple of middleware settings, and you've blocked entire classes of attacks. Yet most sites don't have them.
Check your own site at securityheaders.com. I'll wait. If you got an F, you're in good company. Most of the internet fails this test. Let's fix that.
The Essential Headers
Content-Security-Policy (CSP)
This is the big one. CSP tells browsers where they're allowed to load resources from. A good CSP stops most XSS attacks dead because even if an attacker injects malicious JavaScript, the browser won't execute it.
Starting point (restrictive but safe):
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'
What this does:
- Only load scripts from your own domain
- Only load styles from your own domain (inline styles allowed for now)
- Images can come from your domain, data URIs, or any HTTPS source
- Fonts only from your domain
- AJAX/fetch only to your own domain
This will break things. Intentionally. If you're loading Google Analytics, you'll need to add script-src https://www.google-analytics.com. If you're using a CDN, add that domain. The process of adding exceptions teaches you exactly what third-party code is running on your site.
Pro tip: Start with Content-Security-Policy-Report-Only instead. This logs violations without blocking them, so you can fix issues before going live.
Strict-Transport-Security (HSTS)
Once you have HTTPS (you do have HTTPS, right?), HSTS tells browsers to always use HTTPS, even if someone types http:// or clicks an HTTP link.
Strict-Transport-Security: max-age=31536000; includeSubDomains
This says "for the next year, always use HTTPS for this domain and all subdomains." Once a browser sees this header, it won't even try HTTP. It upgrades automatically.
Warning: make sure HTTPS actually works everywhere before enabling this. If you enable HSTS and your HTTPS is broken, users won't be able to access your site at all.
X-Content-Type-Options
Browsers try to be helpful by guessing content types. If you serve a file without a content type, the browser might decide it's HTML or JavaScript and execute it. This opens up attacks where someone uploads a malicious file that gets served as a script.
X-Content-Type-Options: nosniff
This tells browsers to trust your content types and not guess. Always use it.
X-Frame-Options
Clickjacking is when an attacker embeds your site in an invisible iframe, then tricks users into clicking buttons they can't see. X-Frame-Options prevents embedding.
X-Frame-Options: DENY
Or if you need to allow embedding from your own domain:
X-Frame-Options: SAMEORIGIN
CSP has a more flexible frame-ancestors directive, but X-Frame-Options has better legacy browser support. Use both.
Referrer-Policy
When users click links on your site, the browser tells the destination site where they came from (the referrer). This can leak sensitive information - imagine a URL like yoursite.com/reset-password?token=abc123.
Referrer-Policy: strict-origin-when-cross-origin
This sends the full referrer for same-origin requests, but only the origin (not the path) for cross-origin requests. Balances privacy with functionality.
Permissions-Policy
Modern browsers have powerful features: camera, microphone, geolocation, payment APIs. If you don't use them, disable them so compromised code can't abuse them.
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
The empty parentheses mean "nobody can use this, not even my own site." If you do need a feature, specify which origins can use it.
Implementing the Headers
Nginx
Add to your server block:
add_header Content-Security-Policy "default-src 'self';" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
Express.js
Use the Helmet middleware:
npm install helmet
Then:
const helmet = require('helmet');
app.use(helmet());
Helmet sets sensible defaults for all these headers. Customize as needed.
Vercel/Netlify
Both platforms support header configuration files. For Vercel, add to vercel.json:
"headers": [{ "source": "/(.*)", "headers": [{ "key": "X-Content-Type-Options", "value": "nosniff" }] }]
Cloudflare
Cloudflare lets you set headers via Page Rules or Workers. For simple cases, use Transform Rules in the dashboard.
Testing Your Headers
After adding headers:
- Check securityheaders.com - aim for an A grade
- Test your site thoroughly - CSP especially will break things
- Check browser console for CSP violations
- Test in multiple browsers
Common Pitfalls
Inline scripts and styles: CSP blocks inline JavaScript and CSS by default. You'll need to either move code to external files, use nonces, or (less secure) allow 'unsafe-inline'.
Third-party widgets: Chat widgets, analytics, embedded videos. They all need CSP exceptions. Add them one by one, verifying each is necessary.
API endpoints: Some headers like X-Frame-Options don't make sense for JSON APIs. Be thoughtful about where headers apply.
Caching: Headers get cached too. If you change headers and don't see the effect, clear caches or wait for TTLs to expire.
Beyond Headers
Security headers are a layer, not a complete solution. They complement but don't replace:
- Input validation and output encoding
- Authentication and authorization
- HTTPS everywhere
- Dependency security scanning
- Regular security audits
But unlike those other practices, headers take minutes to implement and immediately block real attacks. If you haven't set them up, do it today. Your future self (and your users) will thank you.