TL;DR
The #1 security headers best practice is to add defense-in-depth headers that protect against XSS, clickjacking, and protocol downgrade attacks. Start with X-Content-Type-Options, X-Frame-Options, and Strict-Transport-Security. Add Content-Security-Policy for best protection. Test with securityheaders.com.
"Security headers are your first line of defense. They cost nothing to implement but protect against entire classes of attacks."
Essential Security Headers
Every web app should ship these headers:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Strict-Transport-Security: max-age=31536000; includeSubDomains
Header-by-Header Explanation
Strict-Transport-Security (HSTS) 2 min
Forces browsers to use HTTPS, preventing protocol downgrade attacks:
Strict-Transport-Security: max-age=31536000; includeSubDomains
// Options:
// max-age: Time in seconds to remember HTTPS-only (1 year recommended)
// includeSubDomains: Apply to all subdomains
// preload: Submit to browser preload list (permanent, use carefully)
HSTS is sticky. Once set, browsers will refuse HTTP connections for the max-age period. We've seen people skip the short test window and get locked into a year of HSTS on a misconfigured domain. Start with a short max-age (3600) to test, then increase to 31536000 (1 year).
Content-Security-Policy (CSP) 5 min
The most powerful header for preventing XSS:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.yourdomain.com; frame-ancestors 'none'
| Directive | Controls | Recommended Value |
|---|---|---|
| default-src | Fallback for all resource types | 'self' |
| script-src | JavaScript sources | 'self' (avoid 'unsafe-inline') |
| style-src | CSS sources | 'self' 'unsafe-inline' |
| img-src | Image sources | 'self' data: https: |
| connect-src | Fetch, XHR, WebSocket | 'self' your-api |
| frame-ancestors | Who can embed you | 'none' or 'self' |
X-Frame-Options 1 min
Stops clickjacking by controlling whether your site can be embedded elsewhere:
X-Frame-Options: DENY // Cannot be embedded anywhere
X-Frame-Options: SAMEORIGIN // Only same origin can embed
X-Frame-Options: ALLOW-FROM uri // Deprecated, use CSP instead
X-Content-Type-Options 1 min
Prevents MIME type sniffing attacks:
X-Content-Type-Options: nosniff
// Without this, browsers might execute a file as JavaScript
// even if served with a different Content-Type
Referrer-Policy 1 min
Controls what referrer information is sent with requests:
Referrer-Policy: no-referrer // Never send referrer
Referrer-Policy: strict-origin-when-cross-origin // Recommended
Referrer-Policy: same-origin // Only for same-origin
Permissions-Policy 1 min
Controls which browser features your site is allowed to touch:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
// Disable features you don't use to reduce attack surface
// () = disabled, (self) = this origin only, (*) = any origin
Framework Configuration
Next.js 2 min
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-XSS-Protection', value: '1; mode=block' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=()' },
],
},
];
},
};
Express 2 min
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
},
}));
Vercel/Netlify 1 min
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
]
}
]
}
Testing Your Headers
Use these tools to verify your configuration:
- securityheaders.com: Comprehensive header analysis
- Mozilla Observatory: Full security audit
- Browser DevTools: Network tab shows response headers
- curl -I: Quick header check from terminal
Learn More: For comprehensive documentation on security headers, see MDN Web Docs: HTTP Security Headers and OWASP Secure Headers Project.
Do I need both X-Frame-Options and frame-ancestors?
For maximum compatibility, use both. CSP frame-ancestors is more flexible and overrides X-Frame-Options in modern browsers, but X-Frame-Options still covers older ones.
Why does CSP break my site?
CSP blocks resources that don't match your policy. Start with report-only mode (Content-Security-Policy-Report-Only) to find violations without breaking anything, then fix and switch to enforcing mode.
Should I use X-XSS-Protection?
Modern browsers have deprecated it, and it can actually introduce vulnerabilities in older ones. Include it for legacy support, but lean on CSP for real protection.
Further Reading
Put these practices into action with our step-by-step guides.
Check Your Security Headers
Scan your site for missing or misconfigured security headers.