Antoine Duno
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
- Security headers are the fastest way to harden a Next.js application. This guide covers all seven essential headers with production-ready configuration for both App Router and Pages Router.
- Includes copy-paste code examples and step-by-step instructions.
- Free automated scan available to verify your implementation.
How to Add Security Headers to Next.js (Complete Guide with Code)
Next.js security headers are among the easiest, highest-impact security improvements you can make to a web application. They are a few lines of configuration in next.config.js and they defend against XSS, clickjacking, MIME sniffing, protocol downgrade attacks, and cross-origin information leaks.
Despite that, most Next.js apps ship without them. If you have never explicitly configured security headers, your application is almost certainly missing several.
This guide covers every essential header, a full working next.config.js configuration, the differences between App Router and Pages Router setups, and how to verify everything is working.
Why Security Headers Matter
HTTP security headers are instructions your server sends to the browser. They tell the browser what it is and is not allowed to do with your content. Without them, browsers apply permissive defaults that attackers can exploit.
| Without the header | Attack enabled |
|---|---|
No Content-Security-Policy | XSS via injected scripts |
No Strict-Transport-Security | SSL stripping / MITM on first visit |
No X-Frame-Options | Clickjacking via iframe |
No X-Content-Type-Options | MIME confusion attacks |
No Referrer-Policy | Sensitive URLs leaked to third parties |
No Permissions-Policy | Unauthorized camera/mic/geolocation access |
No Cross-Origin-Opener-Policy | Cross-origin attacks via window.opener |
Every header in that table takes under 10 lines of code to add.
How next.config.js Headers Work
Next.js lets you define custom HTTP headers via the headers() async function in next.config.js. This function returns an array of header configuration objects, each with a source pattern (which routes get the headers) and a headers array.
// next.config.js (minimal structure)
/** @type {import(''next'').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: "/(.*)", // apply to all routes
headers: [
{ key: "X-Frame-Options", value: "DENY" },
// ... more headers
],
},
];
},
};
module.exports = nextConfig;The source field uses path-to-regexp syntax. Using "/(.*)" applies headers to every route including API routes.
The Seven Essential Security Headers
1. Strict-Transport-Security (HSTS)
Forces browsers to use HTTPS for all future requests to your domain, even if the user types http://.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadmax-age=31536000— remember this for one yearincludeSubDomains— apply to all subdomainspreload— eligible for browser HSTS preload list (submit at hstspreload.org)
Warning: Do not add preload until you are certain every subdomain serves HTTPS. This directive is very hard to reverse once in the preload list.
2. Content-Security-Policy (CSP)
Controls which resources (scripts, styles, fonts, images, iframes) the browser is allowed to load. The most powerful and complex header — covered in depth in the CSP guide.
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''3. X-Frame-Options
Prevents your pages from being embedded in iframes on other domains, blocking clickjacking attacks.
X-Frame-Options: DENYUse DENY unless you specifically need to allow framing from the same origin (SAMEORIGIN).
Note: CSP''s frame-ancestors directive supersedes this header in modern browsers, but X-Frame-Options is still needed for legacy browser compatibility.
4. X-Content-Type-Options
Prevents browsers from MIME-sniffing a response away from the declared Content-Type.
X-Content-Type-Options: nosniffThis is a one-value header. Always set it to nosniff. Without it, a browser might execute a JavaScript file that your server declares as text/plain.
5. Referrer-Policy
Controls how much referrer information is sent when users navigate away from your site.
Referrer-Policy: strict-origin-when-cross-originThis is the recommended default: sends the full URL as referrer for same-origin requests, but only the origin (no path) for cross-origin requests. This prevents sensitive URL paths (like /dashboard/billing) from appearing in third-party analytics.
6. Permissions-Policy
Restricts access to browser APIs — camera, microphone, geolocation, payment, etc. This replaces the older Feature-Policy header.
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()The empty parentheses () explicitly disable the feature for all origins. Only grant permissions your application actually uses.
7. Cross-Origin-Opener-Policy (COOP)
Isolates your browsing context so cross-origin windows cannot access your window object.
Cross-Origin-Opener-Policy: same-originRequired for sites that use SharedArrayBuffer (for Wasm performance) and a good practice for all sites. Prevents attacks via window.opener.
Full Working next.config.js Configuration
This is a production-ready configuration you can drop into a new or existing Next.js project:
// next.config.js
/** @type {import(''next'').NextConfig} */
const securityHeaders = [
{
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=(), payment=(), usb=(), interest-cohort=()",
},
{
key: "Cross-Origin-Opener-Policy",
value: "same-origin",
},
{
key: "Content-Security-Policy",
value: [
"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''",
"frame-ancestors ''none''",
"base-uri ''self''",
"form-action ''self''",
].join("; "),
},
];
const nextConfig = {
async headers() {
return [
{
source: "/(.*)",
headers: securityHeaders,
},
];
},
};
module.exports = nextConfig;App Router vs Pages Router Differences
Pages Router (pages/)
The headers() function in next.config.js applies to all Pages Router routes. CSP with inline scripts is handled by using ''unsafe-inline'' (lower security) or by generating a nonce server-side in _document.js.
App Router (app/)
App Router makes nonce-based CSP significantly easier because you can generate and inject nonces in middleware.ts:
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { nanoid } from "nanoid";
export function middleware(request: NextRequest) {
const nonce = nanoid();
const cspHeader = [
"default-src ''self''",
`script-src ''self'' ''nonce-${nonce}''`,
"style-src ''self'' ''unsafe-inline''",
"img-src ''self'' data: https:",
"font-src ''self'' https://fonts.gstatic.com",
"connect-src ''self''",
"frame-ancestors ''none''",
].join("; ");
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set("content-security-policy", cspHeader);
const response = NextResponse.next({
request: { headers: requestHeaders },
});
response.headers.set("content-security-policy", cspHeader);
return response;
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico).*)",
],
};Then in your root layout, read the nonce and pass it to <Script> components:
// app/layout.tsx
import { headers } from "next/headers";
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
const nonce = headers().get("x-nonce") ?? "";
return (
<html lang="en">
<body>
{children}
<Script
src="https://example.com/analytics.js"
strategy="afterInteractive"
nonce={nonce}
/>
</body>
</html>
);
}This approach is more secure than ''unsafe-inline'' because each nonce is unique per request, making it impossible for injected scripts to execute.
Common Mistakes
Mistake 1: Applying headers only to HTML routes
If your source pattern excludes API routes, your API is not covered. Use "/(.*)" to cover everything.
Mistake 2: Setting HSTS before HTTPS is fully working
If you set HSTS and then your certificate expires or you try to revert to HTTP, users will be locked out of your site for the duration of max-age. Verify your HTTPS is solid before setting HSTS.
Mistake 3: CSP that breaks on first load
A CSP that is too restrictive will break your site silently in production. Before enforcing CSP, use Content-Security-Policy-Report-Only to collect violations without blocking anything:
{
key: "Content-Security-Policy-Report-Only",
value: "default-src ''self''; report-uri /api/csp-report",
}Mistake 4: Not setting frame-ancestors in CSP
If you set X-Frame-Options: DENY but your CSP does not include frame-ancestors ''none'', modern browsers use the CSP directive and ignore X-Frame-Options. Set both for full coverage.
Mistake 5: Forgetting subdomains
Headers configured in next.config.js only apply to your Next.js application. If you have a subdomain running on different infrastructure (a Stripe-hosted payment page, a documentation site), those subdomains need their own header configuration.
How to Test Your Headers
After deploying, there are two fast ways to verify:
Option 1: Run a free ZeriFlow scan
Go to zeriflow.com/free-scan, enter your domain, and the scan will check all seven headers and return a score with specific pass/fail results for each. It takes under 60 seconds and requires no account.
Option 2: curl
curl -I https://yourdomain.comCheck the response headers manually. Look for all seven headers in the output.
Option 3: Browser DevTools
Open the Network tab, reload your page, click the HTML document request, and inspect the Response Headers section.
What to Expect After Adding Headers
On a typical Next.js SaaS app that had no security headers configured, adding all seven headers moves the security score from around 40–50/100 to 75–85/100 in a single deployment. The remaining gap is usually TLS configuration (controlled by your hosting provider) and cookie settings.
The whole process — adding headers, deploying, verifying — takes under 30 minutes for an experienced developer.
Summary
Next.js security headers are configured in next.config.js via the headers() function and apply to all routes matching your source pattern. The seven essential headers — HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and COOP — each defend against a specific class of attack. App Router enables the more secure nonce-based CSP approach via middleware. Verify your deployment with a free ZeriFlow scan to confirm every header is being served correctly.
Scan your Vercel deployment's security headers.
80+ checks in 60 seconds — free.