Security headers protect your web application from common attacks. They tell browsers how to handle your content, what sources to trust, and what features to allow. Next.js doesn't set most security headers by default—you need to configure them.
Why security headers matter
| Attack | Header that helps |
|---|---|
| XSS (Cross-Site Scripting) | Content-Security-Policy |
| Clickjacking | X-Frame-Options |
| MIME sniffing | X-Content-Type-Options |
| Man-in-the-middle | Strict-Transport-Security |
| Information leakage | Referrer-Policy |
Without headers, browsers use permissive defaults that attackers exploit.
Setting headers in next.config.js
Use the headers function for static headers:
// next.config.js
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=(), geolocation=()',
},
],
},
];
},
};
Strict-Transport-Security (HSTS)
HSTS forces HTTPS for your domain:
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload',
}
| Directive | Purpose |
|---|---|
max-age |
Seconds to remember HTTPS-only |
includeSubDomains |
Apply to all subdomains |
preload |
Eligible for browser preload lists |
Only enable HSTS when your entire site works over HTTPS. Once set, browsers will refuse HTTP connections.
HSTS preload
Submit your domain to hstspreload.org for inclusion in browser preload lists. Browsers will enforce HTTPS before the first visit.
Content-Security-Policy
CSP is the most complex but most protective header. It controls what resources can load:
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://trusted-cdn.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join('; '),
}
CSP directives
| Directive | Controls |
|---|---|
default-src |
Fallback for other directives |
script-src |
JavaScript sources |
style-src |
CSS sources |
img-src |
Image sources |
font-src |
Font sources |
connect-src |
XHR, fetch, WebSocket |
frame-ancestors |
Who can embed your page |
base-uri |
Base URL restrictions |
form-action |
Form submission targets |
CSP source values
| Value | Meaning |
|---|---|
'self' |
Same origin |
'none' |
Nothing allowed |
'unsafe-inline' |
Inline scripts/styles (avoid for scripts) |
'unsafe-eval' |
eval() and similar (avoid) |
https: |
Any HTTPS source |
data: |
Data URIs |
'nonce-abc123' |
Specific nonce |
'sha256-...' |
Specific hash |
Report-Only mode
Test CSP without breaking your site:
{
key: 'Content-Security-Policy-Report-Only',
value: "default-src 'self'; report-uri /api/csp-report",
}
The browser reports violations but doesn't block them. Review reports before enforcing.
Dynamic CSP with middleware
For CSP with nonces (needed for strict inline script policies):
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: https:`,
`font-src 'self'`,
`connect-src 'self'`,
`frame-ancestors 'none'`,
].join('; ');
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
response.headers.set('x-nonce', nonce); // Pass to components
return response;
}
Use the nonce in your layout:
// app/layout.tsx
import { headers } from 'next/headers';
export default async function RootLayout({ children }) {
const nonce = headers().get('x-nonce');
return (
<html>
<head>
<script nonce={nonce} src="/analytics.js" />
</head>
<body>{children}</body>
</html>
);
}
X-Frame-Options
Prevents clickjacking by controlling iframe embedding:
{
key: 'X-Frame-Options',
value: 'DENY', // or 'SAMEORIGIN'
}
| Value | Meaning |
|---|---|
DENY |
Never allow embedding |
SAMEORIGIN |
Only same-origin embedding |
CSP's frame-ancestors is more flexible but X-Frame-Options has wider support.
Permissions-Policy
Disable browser features you don't use:
{
key: 'Permissions-Policy',
value: [
'camera=()',
'microphone=()',
'geolocation=()',
'interest-cohort=()', // Disable FLoC
].join(', '),
}
Referrer-Policy
Control referrer information sent with requests:
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
}
| Value | Behavior |
|---|---|
no-referrer |
Never send referrer |
strict-origin-when-cross-origin |
Full path for same-origin, origin only for cross-origin |
same-origin |
Only for same-origin requests |
Complete configuration
// next.config.js
const securityHeaders = [
{
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=(), geolocation=()',
},
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains',
},
];
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders,
},
];
},
};
Testing headers
- SecurityHeaders.com: Scan your deployed site for a grade
- DevTools: Network tab > select request > Headers
- curl:
curl -I https://yoursite.com - Observatory: Mozilla's observatory.mozilla.org
Summary
Security headers protect against XSS, clickjacking, and other attacks. Configure them in next.config.js for static headers or middleware.ts for dynamic values like CSP nonces. Start with Report-Only mode for CSP to identify issues. Test with securityheaders.com and aim for an A grade. Enable HSTS only when fully HTTPS. These headers add significant protection with minimal performance cost.
