Why Security Headers Matter
Security headers tell browsers how to behave when handling your site. Without them, you're vulnerable to:
- Cross-site scripting (XSS)
- Clickjacking
- MIME type sniffing
- Man-in-the-middle attacks
Quick Implementation
next.config.js
// next.config.jsconst securityHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()'
}
]
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders,
},
]
},
}
Header-by-Header Explanation
Strict-Transport-Security (HSTS)
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
}What it does: Forces HTTPS for 2 years, includes subdomains.
Why you need it: Prevents downgrade attacks and cookie hijacking.
X-Frame-Options
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN' // or 'DENY'
}What it does: Prevents your site from being embedded in iframes.
Why you need it: Prevents clickjacking attacks.
Options:
DENY- Never allow framingSAMEORIGIN- Allow framing from same origin only
X-Content-Type-Options
{
key: 'X-Content-Type-Options',
value: 'nosniff'
}What it does: Prevents browser from MIME-sniffing files.
Why you need it: Stops execution of malicious files disguised as other types.
Referrer-Policy
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
}What it does: Controls how much referrer info is sent.
Why you need it: Prevents leaking sensitive URLs to external sites.
Options:
no-referrer- Never sendstrict-origin- Send origin only, HTTPS onlystrict-origin-when-cross-origin- Full URL same-origin, origin only cross-origin
Permissions-Policy
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()'
}What it does: Disables browser features you don't need.
Why you need it: Reduces attack surface from feature abuse.
Common restrictions:
value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()'Content Security Policy (CSP)
CSP is the most powerful (and complex) security header.
Basic CSP
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';"
}CSP for Next.js with Common Services
const ContentSecurityPolicy =
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https: blob:;
connect-src 'self' https://api.stripe.com https://vitals.vercel-insights.com;
frame-src 'self' https://js.stripe.com;
frame-ancestors 'none';
// Clean up whitespace
const cspValue = ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
const securityHeaders = [
// ... other headers
{
key: 'Content-Security-Policy',
value: cspValue
}
]
CSP Directives Explained
| Directive | Controls |
|---|
| default-src | Fallback for all others |
|---|
| script-src | JavaScript sources |
|---|
| style-src | CSS sources |
|---|
| img-src | Image sources |
|---|
| font-src | Font sources |
|---|
| connect-src | Fetch/XHR/WebSocket |
|---|
| frame-src | iframe sources |
|---|
| frame-ancestors | Who can frame you |
|---|
Dealing with 'unsafe-inline' and 'unsafe-eval'
Next.js requires these by default. To remove them:
Option 1: Use nonces (complex)
// middleware.ts
import { NextResponse } from 'next/server'
import crypto from 'crypto'export function middleware(request) {
const nonce = crypto.randomBytes(16).toString('base64')
const csp = script-src 'nonce-${nonce}' 'strict-dynamic';
const response = NextResponse.next()
response.headers.set('Content-Security-Policy', csp)
response.headers.set('x-nonce', nonce)
return response
}
Option 2: Accept 'unsafe-inline' with other protections
For most apps, other headers provide sufficient protection.
Testing Your Headers
Using curl
curl -I https://your-site.comUsing Security Header Tools
- securityheaders.com - Free scanner
- observatory.mozilla.org - Mozilla's tool
- Chrome DevTools - Network tab → Response Headers
Expected Output
HTTP/2 200
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
content-security-policy: default-src 'self'; ...Environment-Specific Headers
// next.config.jsconst isDev = process.env.NODE_ENV === 'development'
const securityHeaders = [
// Always include
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
// Only in production
...(!isDev ? [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
}
] : [])
]
Common Issues
Stripe Elements Not Loading
Add to CSP:
frame-src https://js.stripe.com;
script-src https://js.stripe.com;Google Fonts Not Loading
Add to CSP:
style-src https://fonts.googleapis.com;
font-src https://fonts.gstatic.com;Analytics Blocked
Add to CSP:
script-src https://www.googletagmanager.com;
connect-src https://www.google-analytics.com;Images Not Loading
img-src 'self' data: https:; // Allow all HTTPS images
// or specific domains
img-src 'self' https://images.unsplash.com;Complete next.config.js
// next.config.jsconst ContentSecurityPolicy =
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://www.googletagmanager.com https://js.stripe.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https: blob:;
connect-src 'self' https://api.stripe.com https://vitals.vercel-insights.com https://www.google-analytics.com;
frame-src 'self' https://js.stripe.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()'
}
]
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders,
},
]
},
}
Checklist
[ ] HSTS configured (production only)
[ ] X-Frame-Options set
[ ] X-Content-Type-Options set
[ ] Referrer-Policy set
[ ] Permissions-Policy set
[ ] CSP configured and tested
[ ] All external services allowed in CSP
[ ] Headers verified with curl or online toolThe Bottom Line
Security headers are copy-paste security. Add them to next.config.js, test they work, ship it.
5 minutes of configuration. Significant protection against common attacks.