All posts

How to set up a CSP nonce in Next.js

CentralCSP Team ·

Last update:

You can give a Next.js App a strict, nonce-based Content Security Policy (CSP) in one file. A CSP is an HTTP response header that tells the browser which scripts and other resources a page may load and run. The clean way to do it in Next.js is to generate a fresh nonce per request in proxy.ts, set the policy on both the request and the response, and let Next.js apply that nonce to every script it renders for you.

This is a how-to for the current Next.js (16). It uses Web Crypto so the code runs in the Edge runtime, sets one strict script-src with a nonce and 'strict-dynamic', and shows how to read the nonce when you have your own inline script or a next/script tag. If you are new to nonces in general, get started with Content Security Policy covers what a nonce is and why it works, and the general per-framework nonce setup covers the same pattern across other stacks.

The short version

  1. Generate a per-request nonce in proxy.ts with Web Crypto.
  2. Set the CSP on the forwarded request headers (so Next.js can read the nonce) and on the response (so the browser enforces it).
  3. Let Next.js auto-apply the nonce to the scripts it renders. You do not nonce its script tags yourself.
  4. Force dynamic rendering on any route that uses the nonce. A per-request nonce and static optimization are incompatible.
  5. Read the nonce from headers() in a Server Component only when you have your own inline script or a next/script tag.

A version note before the code

In Next.js 16 the middleware file convention was renamed. The file is now proxy.ts and the exported function is proxy. The official CSP guide ships proxy.ts, so that is what this post uses.

On Next.js 15 and earlier this file is middleware.ts with export function middleware. The body is identical, only the file name and the function name differ.

Step 1: generate the nonce in proxy.ts

The proxy runs in the Edge runtime, so use Web Crypto. This is the one detail that trips people up: Node's crypto.randomBytes and require('crypto') are not available in the Edge runtime, so reaching for them throws. Use crypto.randomUUID(), which is part of Web Crypto and available there:

proxy.ts
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')

A nonce has to be unique per request, unpredictable, and base64. crypto.randomUUID() gives you a fresh, unguessable value on every request from a cryptographically secure source, which is exactly what a nonce needs. The deeper requirements (why it must be per request and from a secure random source) are in get started with Content Security Policy, so this post does not re-derive them.

Step 2: write the proxy

Here is the full current proxy. It builds a strict policy, stamps the nonce into it, and sets the policy in two places:

proxy.ts
import { NextRequest, NextResponse } from 'next/server'

export function proxy(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
  const isDev = process.env.NODE_ENV === 'development'
  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ''};
    style-src 'self' '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;
  `
  const contentSecurityPolicyHeaderValue = cspHeader.replace(/\s{2,}/g, ' ').trim()

  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-nonce', nonce)
  requestHeaders.set('Content-Security-Policy', contentSecurityPolicyHeaderValue) // [!code highlight]

  const response = NextResponse.next({ request: { headers: requestHeaders } })
  response.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue) // [!code highlight]
  return response
}

Two things in this code are doing the real work.

The policy is set on both the forwarded request headers and the response. Setting it on the request (requestHeaders.set('Content-Security-Policy', ...)) is how Next.js sees the nonce and applies it to the scripts it renders. Setting it on the response (response.headers.set('Content-Security-Policy', ...)) is what the browser actually enforces. You need both.

The x-nonce header is a convenience. The proxy puts the raw nonce on a custom request header so your own components can read it back later without parsing the CSP string.

'unsafe-eval' is added in development only. React uses eval in development, so without it the dev server breaks under the policy. The isDev check keeps 'unsafe-eval' out of production entirely, where it would re-enable string-to-code execution and weaken the policy.

The policy here leans on 'strict-dynamic' rather than host allowlists for scripts. 'strict-dynamic' tells the browser to trust scripts that carry the nonce, plus any scripts those load, and to ignore host lists for scripts. That is what makes a nonce policy strong: an attacker who injects a script tag cannot guess the nonce, so it never runs. Avoid reaching for 'unsafe-inline' to silence script errors; it re-enables exactly the inline execution CSP exists to block, and the browser ignores it once a nonce is present anyway.

Scope the proxy so it skips static assets

You usually do not want the proxy generating a nonce for static files and prefetch requests. Add a config.matcher to scope it to the paths that render HTML. The official guide ships a matcher that skips Next.js internals, static files, and prefetches; keep one so the proxy only runs where the nonce is needed.

Step 3: let Next.js nonce its own scripts

This is the part that makes Next.js pleasant to work with. Next.js reads the nonce from the request Content-Security-Policy header and automatically applies it to the scripts it renders. That covers framework scripts, your page bundles, the inline scripts and styles Next.js generates, and any <Script nonce> component.

So you do not manually add a nonce attribute to Next.js's own script tags. Setting the policy on the request header in step 2 is the entire wiring. Next.js does the rest.

Step 4: force dynamic rendering on routes that use the nonce

A per-request nonce only makes sense if every request gets its own rendered HTML. That means the route has to render dynamically. Static optimization, Incremental Static Regeneration (ISR), and Partial Prerendering (PPR) are incompatible with a nonce-based CSP, because they reuse one prerendered response across requests and a reused nonce defeats the point.

Force dynamic rendering with await connection() in the page:

import { connection } from 'next/server'

export default async function Page() {
  await connection()
  // ...
}

Reading headers() in the route (step 5) also opts it into dynamic rendering, so if you already read the nonce there you may not need connection() as well. Use connection() for routes that need dynamic rendering but do not otherwise touch request data.

Step 5: read the nonce for your own inline script

Most of the time you do not need to touch the nonce at all, because Next.js applies it for you. You only read it when you have your own inline script or a next/script tag that Next.js does not nonce automatically.

Read it from the request headers in a Server Component. Note that headers() is async in the current Next.js, so you await it:

import { headers } from 'next/headers'
import Script from 'next/script'

export default async function Page() {
  const nonce = (await headers()).get('x-nonce')
  return (
    <Script
      src="https://api-next.centralcsp.com/script.js"
      strategy="afterInteractive"
      nonce={nonce}
    />
  )
}

The x-nonce header is the one the proxy set in step 2. Pass that value to the nonce prop and the script is trusted by the policy.

Pages Router

If you are still on the Pages Router, the proxy and the nonce flow are the same. What differs is how you read the nonce, because there is no next/headers.

In a page, read it in getServerSideProps from the request headers and pass it as a prop:

export async function getServerSideProps({ req }) {
  const nonce = req.headers['x-nonce'] ?? ''
  return { props: { nonce } }
}

To nonce the document-level scripts, read it in _document.tsx and apply it to <Head> and <NextScript>:

_document.tsx
import Document, { Head, Html, Main, NextScript } from 'next/document'

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx)
    const nonce = ctx.req?.headers?.['x-nonce'] ?? ''
    return { ...initialProps, nonce }
  }

  render() {
    const { nonce } = this.props as { nonce: string }
    return (
      <Html>
        <Head nonce={nonce} />
        <body>
          <Main />
          <NextScript nonce={nonce} />
        </body>
      </Html>
    )
  }
}

export default MyDocument

The App Router is the focus of this post, so this is the short version. The proxy from step 2 is unchanged.

Test in Report-Only first, then watch the reports

Ship the policy on the Content-Security-Policy-Report-Only header before you enforce it. In report-only mode the browser blocks nothing and only reports what the policy would have blocked, so a missed third-party script cannot break the page while you tune it. Point the policy at a reporting endpoint and collect from real traffic:

Reporting-Endpoints: csp-endpoint="https://<Endpoint-ID>.report.centralcsp.com"
Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0mBase64Value' 'strict-dynamic';
  style-src 'self' 'nonce-r4nd0mBase64Value';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  report-to csp-endpoint

To set this on the report-only header in Next.js, swap Content-Security-Policy for Content-Security-Policy-Report-Only in the proxy, add a Reporting-Endpoints header, and append report-to csp-endpoint to the policy string.

CentralCSP collects those Report-Only violation reports, groups them by directive and origin, and shows the scripts running on each page, so you can see exactly which third party a tag needs before you enforce. You can start a free trial, point a Report-Only header at it, and watch the reports arrive. To score the finished policy for weak sources or a missing object-src, run it through the CSP evaluator, and to check what a live site already sends, use the CSP scanner. The full Report-Only-first workflow across every framework is in how to build a strong CSP, and the exact header for each stack is in how to set the CSP header in every framework.

Running Google Tag Manager or GA4 in your Next.js app under this policy works the same way: nonce the GTM bootstrap and let 'strict-dynamic' trust the tags. See CSP with Google Analytics and Tag Manager for the exact snippet and where the Google hosts belong, and GTM under a strict CSP in Next.js for the Next.js-specific walkthrough.

Frequently asked questions

How do I add a CSP nonce in Next.js?

Generate a per-request nonce in proxy.ts with Buffer.from(crypto.randomUUID()).toString('base64'), set the Content-Security-Policy header on both the forwarded request and the response, and include 'nonce-...' and 'strict-dynamic' in script-src. Next.js then applies the nonce to the scripts it renders automatically.

Why does my nonce throw in Next.js middleware or proxy?

Because you are using Node's crypto.randomBytes or require('crypto'), which are not available in the Edge runtime the proxy runs in. Use Web Crypto instead: crypto.randomUUID().

Do I need to add a nonce to every script tag in Next.js?

No. Next.js reads the nonce from the request Content-Security-Policy header and applies it to the scripts it renders, including framework scripts, page bundles, and <Script nonce> components. You only read the nonce yourself for your own inline script or a next/script tag you control.

Why does a nonce break my static pages in Next.js?

A per-request nonce requires dynamic rendering, so it is incompatible with static optimization, ISR, and Partial Prerendering. Force dynamic rendering with await connection(), or read headers() in the route, which also opts it into dynamic rendering.

Is this different on Next.js 15?

Only the file name. On Next.js 15 and earlier the file is middleware.ts with export function middleware. The nonce generation, the dual request and response headers, and the auto-nonce behavior are the same.

The takeaway

A strict nonce-based CSP in Next.js comes down to one proxy: generate the nonce with Web Crypto, set the policy on both the request and the response, and let Next.js nonce its own scripts. Force dynamic rendering on the routes that use it, read the nonce from headers() only for your own inline scripts, and roll the whole thing out in Report-Only first so nothing breaks while you tune it.

Further reading: the Next.js CSP guide, the MDN guide to CSP, and the W3C CSP Level 3 specification.