May 25, 2026
Next.js Content Security Policy in App Router with Nonces and Security Headers
A practical guide to Next.js Content Security Policy in App Router apps covering CSP directives, nonces, security headers, third-party scripts, and rollout testing.
8 min read
Next.js Content Security Policy in App Router with Nonces and Security Headers
If you are searching for Next.js Content Security Policy App Router, you probably already have authentication, route handlers, and form mutations working. The next question is whether the browser is allowed to execute only the scripts, styles, images, frames, and form submissions your app actually expects. That is the job of a Content Security Policy.
This guide shows a practical rollout for Next.js CSP nonce support and Next.js security headers App Router hardening. You will start with an audit-friendly policy, add request-scoped nonces when inline scripts are unavoidable, and test the policy before enforcing it. It pairs naturally with Next.js Middleware Security Best Practices for App Router Apps, Secure API Route Patterns in Next.js for Safer App Router Backends, Next.js CSRF Protection in App Router for Safer Forms and Mutations, and Next.js Script Optimization for Third-Party Performance.
Treat CSP as a browser execution contract
A Content Security Policy is not a replacement for validation, authentication, authorization, escaping, or safe database access. It is a browser-side contract that says which sources are allowed to run code or load content on a page. When an XSS bug slips through, CSP can reduce what the injected payload is able to do.
In App Router applications, CSP is especially useful around:
- authenticated dashboards with sensitive data
- account, billing, and admin pages
- routes that include third-party analytics or widgets
- pages that render user-generated content
- forms protected by cookies and Server Actions
- AI-assisted features that may display generated HTML-like content
The mistake is trying to write the perfect policy in one commit. A strict policy touches scripts, styles, images, fonts, frames, workers, forms, and vendor integrations. Roll it out like production infrastructure: inventory first, report violations, fix known breakage, then enforce.
Start with a reportable Next.js security headers baseline
For many App Router projects, a static header baseline is the easiest first step. It will not solve every inline-script problem, but it gives the team a visible policy and removes dangerous defaults.
In an ESM next.config.mjs, keep the CSP readable, then collapse whitespace for the final header:
const cspHeader = `
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-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`;
const nextConfig = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "Content-Security-Policy-Report-Only",
value: cspHeader.replace(/\s{2,}/g, " ").trim(),
},
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
],
},
];
},
};
export default nextConfig;
This policy is intentionally not the finish line. The unsafe-inline and unsafe-eval entries are compatibility allowances, not a security goal. They are common during the first pass because development tooling, inline bootstrapping, and vendor snippets can break immediately without them. Use Report-Only while you discover what the app actually loads.
For production enforcement, tighten the policy route by route. A marketing page with several embeds may need different allowances than an authenticated dashboard. That route-boundary thinking should match the access-control boundaries from Next.js Middleware Security Best Practices for App Router Apps.
Inventory scripts before tightening script-src
The hardest part of Next.js Content Security Policy App Router work is usually script-src. App Router pages include Next.js runtime scripts, page bundles, inline framework snippets, next/script integrations, and any third-party code your team has added.
Before changing the policy, create a script inventory:
- scripts loaded by root layout
- scripts loaded by nested route layouts
- inline scripts created through
next/script - analytics, tag manager, support, and experiment vendors
- domains used by
connect-srcfor analytics beacons - scripts that require user consent
- scripts that run only on checkout, auth, or admin routes
This is where Next.js Script Optimization for Third-Party Performance becomes security work too. A vendor script that is scoped to one route is easier to allow, review, and remove. A vendor script in the root layout becomes part of every CSP conversation.
Add a Next.js CSP nonce for dynamic pages
When a page genuinely needs inline scripts or framework-generated inline code, a nonce is safer than allowing every inline script. A nonce is generated per request, placed in the CSP header, and attached to scripts that are allowed to execute.
In a Next 14 App Router project, this commonly lives in middleware.ts:
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 isDev = process.env.NODE_ENV !== "production";
const csp = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ""};
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data: https:;
font-src 'self';
connect-src 'self' https:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`;
const value = csp.replace(/\s{2,}/g, " ").trim();
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set("Content-Security-Policy", value);
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
response.headers.set("Content-Security-Policy", value);
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
return response;
}
export const config = {
matcher: [
{
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
missing: [
{ type: "header", key: "next-router-prefetch" },
{ type: "header", key: "purpose", value: "prefetch" },
],
},
],
};
The matcher keeps the nonce work away from static assets, images, and prefetch requests. That matters because request-scoped nonces make pages dynamic. Do not add a nonce to every route unless the security gain is worth the caching and rendering tradeoff.
Pass the nonce to next/script when needed
Next.js can read the nonce from the CSP header and apply it to its own scripts during dynamic rendering. When you render a custom Script component, pass the nonce explicitly so the browser allows it under your policy.
import { headers } from "next/headers";
import Script from "next/script";
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const nonce = (await headers()).get("x-nonce") ?? undefined;
return (
<>
{children}
<Script
id="product-analytics"
src="https://analytics.example.com/client.js"
strategy="afterInteractive"
nonce={nonce}
/>
</>
);
}
Keep this pattern narrow. If an analytics script belongs only in the dashboard, load it in the dashboard layout, not the root layout. If a script requires consent, render it only after consent is known. If a script supports one experiment, delete it when the experiment ends.
Make CSP work with forms, CSRF, and route handlers
CSP should reinforce the server security model. The form-action 'self' directive stops forms from submitting to unexpected origins. The frame-ancestors 'none' directive protects against clickjacking better than relying only on older frame headers. The base-uri 'self' directive prevents injected markup from changing how relative URLs resolve.
These browser controls pair well with server-side checks:
- use Next.js CSRF Protection in App Router for Safer Forms and Mutations for cookie-authenticated mutations
- use Next.js Zod Validation in App Router for Safer Server Actions for payload boundaries
- use Next.js Rate Limiting in App Router for Safer Route Handlers for abuse controls
- use Secure API Route Patterns in Next.js for Safer App Router Backends for auth and authorization close to the data
The order matters. CSP reduces browser execution risk, but your route handler still has to reject unauthorized, malformed, or excessive requests.
Test violations before enforcing
Before switching from Content-Security-Policy-Report-Only to Content-Security-Policy, test the pages that carry the most risk and the most integrations:
- login and signup
- dashboard home
- settings and billing
- admin routes
- pages with analytics or tag manager
- upload flows
- embedded videos, maps, or support widgets
- forms that call Server Actions
Open the browser console and Network panel. A useful policy should produce understandable violations, not a wall of unrelated noise. When a violation appears, decide whether the source is required, scoped correctly, and owned by someone on the team. Do not add broad wildcards just to make the console quiet.
For production monitoring, wire CSP reports to a route that stores only the fields you need. Treat those reports as untrusted input because browsers and clients send them.
export async function POST(request: Request) {
const report = await request.json().catch(() => null);
if (!report || typeof report !== "object") {
return Response.json({ ok: false }, { status: 400 });
}
console.info("csp_violation", {
blocked: report["blocked-uri"],
directive: report["violated-directive"],
document: report["document-uri"],
});
return Response.json({ ok: true });
}
If you expose a report endpoint publicly, protect it like any other route: validate shape, rate limit noisy clients, and avoid logging sensitive full URLs.
Keep the policy under review
A good CSP changes when the product changes. Every new script, iframe, image CDN, analytics endpoint, or payment provider should come with a policy review. That review does not need to be slow. It should answer four questions:
- Which route needs this source?
- Which directive allows it?
- Can it be scoped more narrowly?
- Who owns removing it later?
That discipline keeps Next.js security headers App Router work from becoming a forgotten config blob. A strict policy is useful only when the team understands why each directive exists.
Next steps for App Router teams
Start with Report-Only, inventory the script and connection sources, then enforce the smallest policy that supports the route. Use nonces for dynamic pages that need inline script support, but avoid turning static pages dynamic unless the risk justifies it. Keep third-party scripts scoped, consent-aware, and owned.
Most importantly, treat Next.js Content Security Policy App Router work as one layer in a larger system. CSP helps the browser refuse unexpected execution. Your App Router server code still needs authentication, authorization, validation, CSRF protection, rate limiting, and safe errors at the execution boundary.