Add a strict nonce-based Content Security Policy to Next.js 16
A strict CSP blocks injected scripts even when an XSS bug slips through. Set one up in Next.js 16 with proxy.ts and per-request nonces, then roll it out safely.
Updated 7 min read

In this article
To add a strict Content Security Policy to a Next.js 16 App Router site, generate a random nonce for every request in proxy.ts, put it in a script-src 'nonce-…' 'strict-dynamic' policy, and pass the policy on the request headers so Next.js can attach the nonce to its own scripts. Pages must be dynamically rendered for this to work. Roll it out with Content-Security-Policy-Report-Only first, then enforce it.
This tutorial follows the Next.js 16.4 CSP guide, MDN and Google's strict CSP guidance, and the code was type-checked against Next.js 16.
Why a strict CSP, and not an allowlist
A Content Security Policy tells the browser which scripts, styles and other resources a page may load. Its main job is to limit the damage of cross-site scripting: if an attacker manages to inject <script> into your HTML, the browser refuses to run it.
The traditional approach lists trusted domains, such as script-src 'self' https://cdn.example.com. Google's web.dev guidance explains that such allowlist policies need a lot of customisation and can be bypassed in most configurations, for example through a JSONP endpoint or an old library on an allowed CDN. A strict CSP instead trusts individual script elements, identified by a nonce or a hash. The recommended shape is short:
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';'nonce-{RANDOM}'allows only script elements whosenonceattribute matches. The value must be unpredictable and different for every response; web.dev recommends at least 128 bits.'strict-dynamic'lets a trusted script load further scripts, which bundlers and code-splitting depend on. MDN notes the trade-off: if your scripts build script elements from untrusted input, the CSP will not protect them.object-src 'none'andbase-uriclose plugin and base-tag injection routes.
Nonces or hashes: which fits your Next.js app?
| Approach | How it works in Next.js | Trade-off |
|---|---|---|
Nonce in proxy.ts | Fresh nonce per request; Next.js attaches it to framework scripts, page bundles and <Script> components automatically | Every page must be dynamically rendered: no static optimisation, ISR or CDN caching by default, and Partial Prerendering is incompatible |
| Subresource Integrity (experimental) | experimental.sri adds integrity hashes to scripts at build time | Keeps static generation, but experimental, App Router only, and cannot cover dynamically generated scripts |
Static header with 'unsafe-inline' | Set in next.config.js headers | Easy and cacheable, but not a strict CSP: injected inline scripts still run |
If the site handles accounts, payments or personal data, the nonce approach is usually worth the rendering cost. For a mostly static marketing site, weigh the experimental SRI option or a tight allowlist instead.
Step 1: generate the nonce and policy in proxy.ts
In Next.js 16 the request interceptor is proxy.ts at the project root (or in src/), exporting a function named proxy. This version builds the policy from a map, which is easier to review than one long string, uses 16 random bytes for the nonce, and can switch to report-only mode with an environment variable:
// proxy.ts
import { NextResponse, type NextRequest } from 'next/server';
const REPORT_ONLY = process.env.CSP_REPORT_ONLY === 'true';
function buildCsp(nonce: string, isDev: boolean): string {
const directives: Record<string, string[]> = {
'default-src': ["'self'"],
'script-src': ["'self'", `'nonce-${nonce}'`, "'strict-dynamic'", ...(isDev ? ["'unsafe-eval'"] : [])],
'style-src': ["'self'", isDev ? "'unsafe-inline'" : `'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': [],
'report-to': ['csp-endpoint'],
};
return Object.entries(directives)
.map(([name, values]) => [name, ...values].join(' '))
.join('; ');
}
export function proxy(request: NextRequest) {
// 16 random bytes = 128 bits, base64-encoded
const nonce = Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString('base64');
const csp = buildCsp(nonce, process.env.NODE_ENV === 'development');
// Next.js reads the nonce from the request's CSP header while rendering
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
requestHeaders.set('Content-Security-Policy', csp);
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set(
REPORT_ONLY ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy',
csp,
);
response.headers.set('Reporting-Endpoints', 'csp-endpoint="https://example.com/csp-reports"');
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' },
],
},
],
};Three details matter here. The policy is set on the request headers because Next.js extracts the nonce from the 'nonce-{value}' pattern in that header during server rendering. Development needs 'unsafe-eval', which the Next.js guide says React uses for debugging information such as reconstructed server error stacks; neither React nor Next.js uses eval in production by default. And the matcher skips static assets and prefetch requests, as the Next.js guide recommends.
The Next.js guide's own example creates the nonce with Buffer.from(crypto.randomUUID()).toString('base64'), which also works; the version above simply uses raw random bytes to meet web.dev's 128-bit recommendation explicitly.

Step 2: make pages dynamic and use the nonce
Static pages are built before any request exists, so they cannot contain a per-request nonce. Next.js warns that pages may build successfully but fail at runtime if they are not dynamically rendered. Reading request data opts a page in; for pages that do not otherwise touch the request, call connection():
// app/page.tsx
import { connection } from 'next/server';
export default async function Page() {
await connection(); // render at request time so the nonce is fresh
return <h1>Dashboard</h1>;
}Next.js applies the nonce automatically to its framework scripts, page bundles and its own inline scripts and styles. For third-party scripts, read the nonce from the x-nonce header and pass it to <Script>:
// app/layout.tsx
import { headers } from 'next/headers';
import Script from 'next/script';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const nonce = (await headers()).get('x-nonce') ?? undefined;
return (
<html lang="en">
<body>
{children}
<Script src="https://analytics.example.com/script.js" strategy="afterInteractive" nonce={nonce} />
</body>
</html>
);
}Because of 'strict-dynamic', a nonced loader script can load its own dependencies. You still need to allow the domains it connects to or loads images from, for example in connect-src and img-src.
Step 3: roll out in report-only mode
A browser enforces Content-Security-Policy immediately, so a mistake can break checkout or sign-in for every user. Start with CSP_REPORT_ONLY=true. MDN describes Content-Security-Policy-Report-Only as a policy that is not enforced, while violations are still sent to the reporting endpoint. Note that a report-only policy cannot be delivered in a <meta> element; it must be a header.
Reports go to the endpoint named in Reporting-Endpoints and referenced by the report-to directive. They arrive as POST requests with Content-Type: application/reports+json and a "type": "csp-violation" entry. The older report-uri directive is deprecated, but MDN advises declaring both until report-to is supported in all browsers. Store the reports, group them by blocked URL and directive, and fix or allow each legitimate source.
When a week of normal traffic produces only noise (browser extensions are a common source), switch the header to enforcing mode and keep reporting on.

Step 4: verify
- Request a page twice with
curl -sI https://your-site.example/and confirm the header is present and the nonce changes on every response. - View the page source and check that Next.js scripts carry a
nonceattribute matching the header. - Open the browser console: blocked resources are reported as CSP violations.
- Add a test that injects
<script>alert(1)</script>through a field you render as HTML in a staging build, and confirm the browser blocks it.
Gotchas
- Caching a nonce defeats it. A CDN or full-page cache that serves the same HTML to many users also serves the same nonce. Do not cache nonce-bearing HTML across requests.
'unsafe-inline'is ignored when a nonce is present. Browsers that support nonces ignore it, so it is only a fallback for very old browsers, not a quick fix for blocked inline code.- Inline event handlers such as
onclick="…"in raw HTML cannot carry a nonce. Move them into script code. - Styles. CSS-in-JS libraries must support nonces, or move styles into stylesheets. WebAssembly needs
'wasm-unsafe-eval'. frame-ancestorsonly works as an HTTP header, whichproxy.tsprovides; it is not honoured in a meta tag.- Partial Prerendering is incompatible with nonce-based CSP, according to the Next.js guide.
What to do next
Ship the proxy in report-only mode this week, with a simple endpoint that stores reports. Review them after a few days, fix legitimate sources, then enforce. Once enforced, add the CSP header check to your deployment smoke tests so a refactor of proxy.ts cannot silently remove it.
Frequently asked questions
Does a nonce-based CSP work with static pages in Next.js?
No. A nonce must be unique per response, so pages must be dynamically rendered. For static pages, consider Next.js's experimental Subresource Integrity support or a hash-based policy.
Is middleware.ts still used for CSP in Next.js 16?
The current Next.js documentation uses
proxy.ts, exporting a function namedproxy, as the place to generate nonces and set headers before a page renders.Why does development need 'unsafe-eval'?
The Next.js guide says React uses
evalin development for enhanced debugging information, such as reconstructing server-side error stacks in the browser. It is not required in production.How long should a CSP nonce be?
Google's strict CSP guidance recommends a cryptographically random value of at least 128 bits, Base64-encoded and regenerated for every response. Sixteen random bytes meets that.
Should I use report-to or report-uri?
report-towith aReporting-Endpointsheader is the current mechanism andreport-uriis deprecated. MDN advises declaring both untilreport-tois supported in all browsers.Does a CSP replace output encoding and sanitisation?
No. A CSP is defence in depth. You still need to escape output, sanitise any HTML you render and avoid dangerous sinks; the CSP limits the damage if one of those controls fails.
Your next customer could start right here.
Introduce your tool, product or business to readers exploring technology.
Explore placements and request a private quote
Sources
- How to set a Content Security Policy (CSP) for your Next.js application — Next.js (Vercel)
- Content Security Policy (CSP) — MDN Web Docs
- Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP) — web.dev (Google)
- proxy.js file convention — Next.js (Vercel)
Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.
Comments
No comments yet. Be the first to join the conversation.
Leave a comment
Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.
Related tools
- Directory profile
Figma
Browser-based design and prototyping tool with FigJam whiteboards, Dev Mode for developer handoff, Slides and AI prompt-to-code features.
A tool connected to a service selected for this article. Compare its fit for your project; inclusion is not an endorsement.
- Directory profile
GitHub
Git-based platform for hosting code, reviewing pull requests, tracking issues and running CI/CD workflows, with free and per-user paid plans.
A tool connected to a service selected for this article. Compare its fit for your project; inclusion is not an endorsement.
- Directory profile
GitLab
DevSecOps platform combining Git repositories, merge request reviews, CI/CD pipelines and issue tracking, offered as SaaS, dedicated or self-managed.
A tool connected to a service selected for this article. Compare its fit for your project; inclusion is not an endorsement.
More from Cloudsoft Zone

TutorialJavaScript
JavaScript using declarations: automatic cleanup without finally
Learn JavaScript's using and await using declarations, Symbol.dispose and DisposableStack, with tested browser recipes and October 2026 support data.
By Cloudsoft Zone

Architecture guideJavaScript
Ship JavaScript without a bundler: import maps and JSON modules
How to structure a front end as native ES modules: import maps for bare imports, JSON modules, modulepreload against waterfalls, and cache-busting.
By Cloudsoft Zone

ComparisonJavaScript
structuredClone vs JSON vs spread: deep copy JavaScript safely
Spread is shallow, JSON loses dates, Maps, Sets and undefined, and structuredClone keeps them. A tested comparison of JavaScript copy methods and their limits.
By Cloudsoft Zone
