Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
DevelopmentJavaScriptTutorial

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.

Cloudsoft Zone

Updated 7 min read

Round sixteenth-century iron and leather shield with painted decoration and a central metal boss
Photograph: Ángel M. Felicísimo / CC BY-SA 4.0, via Wikimedia Commons
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:

HTTP
Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
  • 'nonce-{RANDOM}' allows only script elements whose nonce attribute 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' and base-uri close plugin and base-tag injection routes.

Nonces or hashes: which fits your Next.js app?

ApproachHow it works in Next.jsTrade-off
Nonce in proxy.tsFresh nonce per request; Next.js attaches it to framework scripts, page bundles and <Script> components automaticallyEvery 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 timeKeeps static generation, but experimental, App Router only, and cannot cover dynamically generated scripts
Static header with 'unsafe-inline'Set in next.config.js headersEasy 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:

TypeScript
// 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.

Black and white photo of a woman walking out through the open round-edged door of a bank vault
A nonce works like a single-use key: only scripts carrying this response's value are allowed to run.

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():

TSX
// 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>:

TSX
// 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.

Firefox developer tools welcome screen listing Inspector, Console, Debugger, Network and other panels
Blocked resources show up as CSP violations in the browser console, and in your reports once report-to is configured.

Step 4: verify

  1. Request a page twice with curl -sI https://your-site.example/ and confirm the header is present and the nonce changes on every response.
  2. View the page source and check that Next.js scripts carry a nonce attribute matching the header.
  3. Open the browser console: blocked resources are reported as CSP violations.
  4. 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-ancestors only works as an HTTP header, which proxy.ts provides; 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 named proxy, 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 eval in 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-to with a Reporting-Endpoints header is the current mechanism and report-uri is deprecated. MDN advises declaring both until report-to is 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.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. How to set a Content Security Policy (CSP) for your Next.js application — Next.js (Vercel)
  2. Content Security Policy (CSP) — MDN Web Docs
  3. Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP) — web.dev (Google)
  4. 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.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.