May 26, 2026
Next.js Security Headers for App Router Apps
A practical Next.js security headers guide for App Router apps covering HSTS, Referrer-Policy, Permissions-Policy, CSP, middleware, and deployment checks.
7 min read
Next.js Security Headers for App Router Apps
If you are searching for Next.js security headers, you probably already understand that authentication, validation, and authorization do most of the application security work. Headers add a different layer. They tell the browser how to handle transport, referrer data, MIME sniffing, framing, powerful APIs, and script execution before your React code gets a chance to run.
This guide focuses on practical Next.js App Router security headers for production apps. You will build a baseline in next.config.mjs, learn when Next.js middleware security headers are useful, and see how to test the final response instead of assuming deployment handled it. It pairs naturally with Next.js Content Security Policy in App Router with Nonces and Security Headers, Next.js Middleware Security Best Practices for App Router Apps, and Secure API Route Patterns in Next.js for Safer App Router Backends. If your headers protect cookie-authenticated forms, keep Next.js CSRF Protection in App Router for Safer Forms and Mutations nearby too.
Security headers are not magic. A weak route handler is still weak with perfect headers. But a missing header can make a real bug easier to exploit, leak more information than needed, or let browser features stay available on routes that do not need them.
Start with a Next.js security headers baseline
Most App Router projects should start with headers that apply to normal page responses:
Strict-Transport-SecurityX-Content-Type-OptionsReferrer-PolicyPermissions-PolicyContent-Security-PolicyorContent-Security-Policy-Report-Only
Some older lists still include X-Frame-Options and X-XSS-Protection. Be careful with cargo-culting those. X-Frame-Options can still help old clients, but modern clickjacking control belongs in CSP with frame-ancestors. X-XSS-Protection is obsolete enough that many teams intentionally omit it instead of trusting inconsistent browser behavior.
Here is a conservative baseline for next.config.mjs:
const securityHeaders = [
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=(), payment=()",
},
{
key: "Content-Security-Policy-Report-Only",
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' blob: data: https:",
"font-src 'self'",
"connect-src 'self' https:",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join("; "),
},
];
/** @type {import("next").NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: "/(.*)",
headers: securityHeaders,
},
];
},
};
export default nextConfig;
This gives you a visible starting point without pretending the CSP is final. For a strict CSP with nonces, use the deeper rollout in Next.js Content Security Policy in App Router with Nonces and Security Headers.
Understand what each header is doing
The most common failure with Next.js security headers is copying a list without knowing the tradeoffs. You should be able to explain each line during review.
Strict-Transport-Security tells browsers to use HTTPS for future visits. It should only be enabled once HTTPS is correct for the production domain and subdomains. Do not casually add includeSubDomains if you have old subdomains that still depend on HTTP. Do not request preload until you understand the operational commitment.
X-Content-Type-Options: nosniff tells browsers not to guess a different MIME type than the one the server declared. This is small, boring, and usually worth setting everywhere.
Referrer-Policy: strict-origin-when-cross-origin keeps full referrer details for same-origin navigation while sending only the origin to secure cross-origin destinations. That is a practical default for product apps because it preserves useful analytics without leaking full paths and query strings to every external site.
Permissions-Policy disables browser capabilities your app does not need. A dashboard that never uses camera, microphone, geolocation, or payment APIs should not leave those surfaces open by default. Add permissions route by route when a feature actually needs them.
Content-Security-Policy controls where scripts, styles, images, frames, forms, and connections may load from. It is the most powerful header in the set, and also the easiest to break during rollout. Start in report-only mode, inventory violations, then enforce route by route.
Use static headers for stable page-wide policy
next.config.mjs is the right place for headers that do not depend on the request. It is simple, centralized, and easy to review in pull requests. Use it for a baseline that applies to every route or a broad path group.
For example, an admin area can have a tighter Permissions-Policy and a stricter framing rule:
const adminHeaders = [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "same-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
{
key: "Content-Security-Policy",
value: "default-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'",
},
];
const nextConfig = {
async headers() {
return [
{
source: "/admin/:path*",
headers: adminHeaders,
},
];
},
};
Path-scoped headers are useful because the risk profile is not identical across the app. A public marketing page may load video embeds and analytics. An authenticated billing screen should usually have fewer third-party dependencies and tighter browser permissions.
Use Next.js middleware security headers when policy is dynamic
Static config is not always enough. Next.js middleware security headers are useful when the header value depends on request context, such as a per-request CSP nonce, tenant-specific allowed origins, preview deployment behavior, or a route group that needs runtime branching.
Keep middleware narrow. It runs before matching routes complete, so broad middleware can add latency and complexity to every request.
import { NextResponse, type NextRequest } from "next/server";
function createNonce() {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
return btoa(String.fromCharCode(...bytes));
}
export function middleware(request: NextRequest) {
const nonce = createNonce();
const response = NextResponse.next();
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' blob: data: https:",
"font-src 'self'",
"connect-src 'self' https:",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join("; ");
response.headers.set("Content-Security-Policy", csp);
response.headers.set("x-nonce", nonce);
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
return response;
}
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*"],
};
This pattern keeps nonce generation limited to routes that need it. If a public blog page does not render inline scripts, it should not pay the dynamic rendering cost just because one dashboard widget needs a nonce.
Avoid header mistakes that create false confidence
Security headers are easy to add and easy to misunderstand. Watch for these mistakes:
- setting HSTS before every production hostname supports HTTPS
- adding
preloadwithout planning for long-term HTTPS-only behavior - using
Permissions-Policy: *style allowances instead of denying unused APIs - setting
Access-Control-Allow-Origin: *on authenticated API routes - keeping CSP in report-only mode forever
- fixing CSP violations by adding broad wildcards
- trusting scanner grades more than actual route behavior
- assuming headers replace CSRF tokens, validation, or authorization
The CORS mistake deserves special attention. CORS headers are response permissions for browsers, not authentication. If a route handles cookies, sessions, admin data, or private user data, do not make it broadly readable just to satisfy a frontend error. Fix the calling origin and credential model instead.
Test deployed headers, not just local config
The final browser response matters more than the file you edited. Deployment platforms, proxies, CDNs, middleware, route handlers, and rewrites can all affect headers.
After deployment, test the routes that matter:
curl -I https://example.com
curl -I https://example.com/dashboard
curl -I https://example.com/api/account
You can also add a small smoke test for critical routes:
import { test, expect } from "@playwright/test";
test("dashboard sends security headers", async ({ request }) => {
const response = await request.get("/dashboard");
const headers = response.headers();
expect(headers["x-content-type-options"]).toBe("nosniff");
expect(headers["referrer-policy"]).toBe("strict-origin-when-cross-origin");
expect(headers["content-security-policy"]).toContain("frame-ancestors 'none'");
});
Do not test every header on every page. Test representative high-risk routes: dashboard, settings, billing, admin, upload, and any route that renders third-party scripts or user-generated content.
Review headers when the product changes
Headers should be part of feature review, not a one-time checklist. Any new script, iframe, analytics vendor, payment provider, AI widget, upload flow, or cross-origin API call can change the policy.
A useful review asks:
- Which route needs this capability?
- Which header allows or denies it?
- Can the permission be scoped to fewer routes?
- Does the server still validate and authorize the request?
- How will we test the deployed response?
That keeps Next.js App Router security headers connected to real application behavior. The browser layer reinforces your architecture instead of becoming a stale list nobody wants to touch.
Final takeaway
Next.js security headers are a practical browser hardening layer for App Router apps. Start with static headers in next.config.mjs, use middleware only when request-specific policy is needed, and test the deployed response on routes that carry real risk.
The best baseline is boring: HSTS after HTTPS is stable, nosniff, a privacy-aware referrer policy, denied-by-default permissions, and a CSP rollout that moves from report-only to enforcement. Combined with secure route handlers, CSRF protection, middleware boundaries, and validation, headers become a small repeatable part of shipping safer Next.js applications.