Next.js Security Headers: A Copy-Pasteable Setup and Verification Guide
If you want the short answer: add an async headers() function to next.config.js that returns an array of { source, headers: [{ key, value }] } objects, apply it to every path with source: '/(.*)', and set the six headers that matter — HSTS, Content-Security-Policy, X-Frame-Options (or CSP frame-ancestors), X-Content-Type-Options, Referrer-Policy, and Permissions-Policy. Then verify what your deployed site actually returns with a security headers checker, because a header that lives only in your config but never reaches the browser does nothing.
This guide walks through each header, a sane starting value, exactly how to set it in Next.js, and how to confirm it works.
Which security headers does Next.js actually need?
Next.js does not ship most security headers by default, so it's on you to add them. Here's the practical set, what each does, and a starting value you can refine later. Each header links to its MDN reference.
| Header | What it does | Recommended starting value |
|---|---|---|
| Strict-Transport-Security | Forces HTTPS, blocking protocol-downgrade and SSL-strip attacks | max-age=63072000; includeSubDomains; preload |
| Content-Security-Policy | Controls which sources can load scripts, styles, frames, etc. — your main XSS defense | default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none' (tune per app) |
| X-Frame-Options | Stops your pages being embedded in frames (clickjacking) | SAMEORIGIN (or DENY) |
| X-Content-Type-Options | Disables MIME sniffing; the only valid value is nosniff | nosniff |
| Referrer-Policy | Limits how much URL data leaks to third parties | strict-origin-when-cross-origin |
| Permissions-Policy | Scopes powerful browser features (camera, mic, geolocation) | camera=(), microphone=(), geolocation=() |
A few notes before you copy values blindly:
- HSTS
preloadis hard to undo. Only addincludeSubDomains; preloadonce every subdomain serves HTTPS. The OWASP Secure Headers Project and the HSTS spec, RFC 6797, both stress thatmax-ageshould be long for real protection. - CSP is the one header you must tune. A
default-src 'self'policy will break any third-party script, analytics, or font CDN you use until you allow it explicitly. frame-ancestorssupersedesX-Frame-Optionsin modern browsers, per the Next.js headers docs. Keeping both is fine for older clients.
How do I add security headers in next.config.js?
The canonical mechanism is the headers key in next.config.js — an async function returning an array of objects, each with a source (path pattern) and a headers array of { key, value } pairs. This is verified against the official Next.js headers reference.
Here's a complete, working starting point applied to every route:
// next.config.js
const securityHeaders = [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
{
// Tune this to your app — see the CSP section below.
key: 'Content-Security-Policy',
value:
"default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; upgrade-insecure-requests;",
},
]
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders,
},
]
},
}
module.exports = nextConfig
source: '/(.*)' matches every path. You can scope headers to a subtree (e.g. '/api/:path*') if you need different rules for API routes versus pages. The headers apply identically to the App Router and the Pages Router — next.config.js sits above both.
How do I set a strict CSP with nonces in the App Router?
The static CSP above is fine if you can live with 'unsafe-inline' or you have no inline scripts. But a truly strict policy that blocks inline injection needs a per-request nonce, and a nonce can't live in a static config file — it has to be generated fresh on every request.
In the App Router, Next.js does this in middleware (the file Next.js now calls proxy). You generate a random nonce, build the CSP string with 'nonce-...' and 'strict-dynamic', set it on the request and response, and Next.js automatically attaches that nonce to its own framework and bundle scripts. This is the approach documented in the Next.js CSP guide:
// middleware.ts (Next.js may name this proxy.ts)
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const isDev = process.env.NODE_ENV === 'development'
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ''};
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
const contentSecurityPolicyHeaderValue = cspHeader
.replace(/\s{2,}/g, ' ')
.trim()
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-nonce', nonce)
requestHeaders.set('Content-Security-Policy', contentSecurityPolicyHeaderValue)
const response = NextResponse.next({ request: { headers: requestHeaders } })
response.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue)
return response
}
Three things to know about the nonce approach, straight from the docs:
- It forces dynamic rendering. Nonces are injected during server-side rendering per request, so static optimization, ISR, and Partial Prerendering are disabled for pages covered by the policy. That has real performance and caching cost.
'unsafe-eval'is needed in development only. React usesevalfor debugging in dev; production does not.- Read the nonce with
headers()(e.g.(await headers()).get('x-nonce')) when you add your own<Script>tags or third-party scripts.
If you'd rather keep static generation, Next.js also offers experimental hash-based CSP via Subresource Integrity — but for most apps, the static next.config.js CSP or the nonce-based middleware covers it. Either way, treat the CSP value as something you iterate on, watching the browser console for violations and tightening from there.
How do I verify my Next.js security headers are correct?
Setting headers in config is half the job. The browser only enforces what your deployed site actually returns — and that can differ from your config. A CDN or reverse proxy (Vercel, Cloudflare, nginx) might strip, add, or override headers. A header scoped to the wrong source might not match your real routes. A typo in a CSP directive silently weakens the whole policy.
Quick local check with curl:
curl -sI https://your-domain.com | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy'
For a real verdict, run an external security headers checker against your live URL. A black-box scanner like SteelSuit requests your deployed site, inspects the response headers it actually receives, and flags what's missing or weak — it detects your stack and CDN (including Next.js, Vercel, and Cloudflare) from the response, with no source-code access and nothing to install. Concretely, SteelSuit's header check:
- Flags any missing header from the set above (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy).
- Acts as an HSTS checker — it parses your
max-ageand warns when it's shorter than six months, since a brief window leaves a downgrade gap. - Runs a CSP evaluator that flags
'unsafe-inline'and'unsafe-eval'inscript-src/default-src, wildcard (*) sources in critical directives, and missing directives likeobject-src,base-uri, andscript-src.
Because it reads the real response, a checker catches the gap between "I set this header" and "the browser received this header" — the failure mode that config inspection alone can't surface.
How to fix security headers when the scan flags them
If a scan reports missing or weak headers, the fix usually maps directly back to your next.config.js or middleware:
- Missing header entirely → add it to the
headers()array (or middleware). Re-deploy and re-scan. - HSTS too short → raise
max-ageto at least31536000(one year);63072000is a common two-year value. - CSP allows
unsafe-inline→ move inline scripts/styles to nonce-based or hash-based CSP as shown above. - CSP uses
*→ replace wildcards with the specific origins you load from. - Header set in config but not on the live response → check your CDN/proxy; on Vercel and Cloudflare a platform-level rule can override Next.js. Fix it at whichever layer terminates the request last.
For a broader pass beyond headers, see our web app security checklist, and if your concern is leaked credentials in your shipped bundle, find exposed API keys on your website. To make sense of what a scan returns, our guide on reading a vulnerability scan report walks through the output.
Security headers are one of the highest-leverage, lowest-effort hardening steps for any Next.js app — a few entries in next.config.js, one verification scan, and you've closed a whole class of clickjacking, downgrade, and injection gaps.
Frequently asked
What security headers does a Next.js app need?
At minimum: Strict-Transport-Security (HSTS), Content-Security-Policy (CSP), X-Frame-Options or CSP frame-ancestors, X-Content-Type-Options: nosniff, Referrer-Policy, and Permissions-Policy. These cover transport security, XSS and injection defense, clickjacking, MIME sniffing, referrer leakage, and browser-feature scoping.
How do I set a Content-Security-Policy in Next.js?
For a basic policy, add a Content-Security-Policy entry to the headers() function in next.config.js applied to source '/(.*)'. For a strict, nonce-based CSP in the App Router, generate the nonce and policy per request in middleware (the proxy file) so Next.js can attach the nonce to its own scripts. Both approaches must be tuned to the origins your app actually loads.
How do I check if my security headers are correct?
Run a security headers checker against your deployed URL. A checker reads the headers your live site actually returns and flags missing or weak ones — including a too-short HSTS max-age or a CSP that still allows unsafe-inline. This catches gaps your config can't show, like a CDN stripping or overriding a header.
Is X-Frame-Options still needed if I use CSP frame-ancestors?
frame-ancestors in CSP supersedes X-Frame-Options in modern browsers and is more flexible. Setting both is a reasonable belt-and-suspenders choice for older clients, but frame-ancestors is the primary control going forward.
What is a good HSTS value for Next.js?
A common starting value is max-age=63072000; includeSubDomains; preload (two years). Only add includeSubDomains and preload once every subdomain reliably serves HTTPS, because preload is hard to reverse. A short max-age provides weaker protection against downgrade attacks.