How to Fix: CSP with nonce does not work

6 min read

CSP nonces fail in Next.js when the server generates one value but the rendered scripts use another.

This bug usually shows up as browser console errors saying an inline script was refused by Content Security Policy, even though a nonce appears to be present. In practice, the nonce attached to the response header and the nonce attached to the rendered script tags are not coming from the same request lifecycle, so the browser rejects them.

Understanding the Root Cause

A CSP nonce only works when the exact same nonce value is used in two places for the same HTTP response:

  • inside the Content-Security-Policy header, for example script-src 'self' 'nonce-abc123'
  • on every allowed inline <script> tag as nonce="abc123"

In Next.js, this breaks when the nonce is generated in one layer but consumed in another layer that does not share the same value. Common causes include:

  • generating a new nonce in both middleware and rendering code
  • setting the CSP header on the response but never exposing the nonce to the app tree
  • using development mode where Next.js injects extra scripts that do not match your strict CSP expectations
  • reading the nonce too late or from the wrong source, such as a different request context

The key technical issue is that nonces are request-scoped. If your app creates nonceA for the header and nonceB for rendered markup, the browser compares them and blocks the script. It does not matter that both look valid individually.

In Next.js, the safest pattern is:

  1. generate one nonce per request
  2. attach that nonce to the request headers forwarded to the app
  3. build the CSP response header from that same value
  4. read the forwarded nonce in the server-rendered layout or document and apply it to scripts

Step-by-Step Solution

The fix is to create a single source of truth for the nonce. A common implementation is to generate it in middleware, forward it through a request header, and reuse it everywhere else.

1. Generate one nonce in middleware

Create or update middleware.ts so it generates a nonce once per request and uses that same value for both the forwarded request headers and the CSP response header.

import { NextRequest, NextResponse } from 'next/server'

function generateNonce() {
  return Buffer.from(crypto.randomUUID()).toString('base64')
}

export function middleware(request: NextRequest) {
  const nonce = generateNonce()

  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-nonce', nonce)

  const csp = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'nonce-${nonce}'`,
    "img-src 'self' data: blob:",
    "font-src 'self' data:",
    "connect-src 'self' ws: wss:",
    "object-src 'none'",
    "base-uri 'self'",
    "frame-ancestors 'none'"
  ].join('; ')

  const response = NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  })

  response.headers.set('Content-Security-Policy', csp)
  return response
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

This is the critical part: do not generate the nonce again anywhere else.

2. Read the nonce from request headers in the server layer

If you are using the App Router, read the nonce from headers() inside your root layout.

import { headers } from 'next/headers'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const nonce = headers().get('x-nonce') || undefined

  return (
    <html lang="en">
      <body>
        {children}
      </body>
    </html>
  )
}

If you need the nonce for inline scripts, pass it explicitly to the component rendering them.

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

export default function Page() {
  const nonce = headers().get('x-nonce') || undefined

  return (
    <>
      <Script
        id="custom-inline-script"
        nonce={nonce}
        dangerouslySetInnerHTML={{
          __html: "console.log('nonce protected script')",
        }}
      />
    </>
  )
}

3. If using the Pages Router, apply the nonce in _document.tsx

For older Next.js setups, the nonce often belongs in the custom document.

import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'

class MyDocument extends Document<{ nonce?: string }> {
  static async getInitialProps(ctx: DocumentContext) {
    const initialProps = await Document.getInitialProps(ctx)
    const nonce = ctx.req?.headers['x-nonce'] as string | undefined

    return {
      ...initialProps,
      nonce,
    }
  }

  render() {
    const nonce = this.props.nonce

    return (
      <Html>
        <Head nonce={nonce} />
        <body>
          <Main />
          <NextScript nonce={nonce} />
        </body>
      </Html>
    )
  }
}

export default MyDocument

This ensures the framework-generated scripts also receive the same nonce value.

4. Do not validate CSP behavior only from development mode

When reproducing this issue with yarn dev, remember that development mode can inject additional scripts for Fast Refresh, error overlays, and debugging. These can make CSP behavior look broken even when production is configured correctly.

Always verify with a production build:

yarn build
yarn start

If the issue only appears in dev, your nonce setup may still be correct for production, but your CSP policy is too strict for development tooling.

5. Confirm the header and DOM use the same nonce

Open browser devtools and compare:

  • the response header Content-Security-Policy
  • the rendered nonce attribute on blocked or expected scripts

If they differ, the app is still generating or propagating multiple nonce values.

6. Prefer nonces only where required

If you can avoid inline scripts entirely, CSP becomes much easier. But when Next.js or third-party integrations require inline script execution, the nonce must be attached consistently.

Common Edge Cases

  • Middleware excluded from some routes: if your matcher skips a page, that request will not receive the nonce header or CSP header.
  • Static optimization: fully static pages do not have a per-request server lifecycle, which makes per-request nonces incompatible unless the page is forced dynamic.
  • Third-party inline scripts: analytics, tag managers, or consent tools often inject inline code that also needs the same nonce or a different CSP strategy.
  • Using both App Router and Pages Router: mixed rendering paths can make nonce propagation inconsistent if only one side is wired correctly.
  • CDN or proxy rewriting headers: an upstream proxy can strip or overwrite the CSP header, causing valid markup to fail in the browser.
  • Style nonces: if your policy includes strict style-src, inline styles or framework-injected style tags may also need a matching nonce.
  • Dev-only console noise: some script violations during local development are caused by Next.js tooling rather than your production rendering path.

FAQ

Why does the nonce look correct but the browser still blocks the script?

Because the browser does an exact match against the nonce listed in the CSP header. If the script tag nonce came from a different value, or the policy header was modified in transit, the script is blocked.

Why does this often fail in yarn dev but behave differently in production?

Next.js development mode injects additional tooling scripts for hot reload and debugging. A very strict CSP can block those scripts, so always verify with yarn build and yarn start before concluding the implementation is broken.

Can I generate the nonce inside a React component?

No. That usually creates a new value during rendering, which will not match the nonce already embedded in the response header. The nonce should be generated once at the request boundary, typically in middleware or the server entry path.

The durable fix is simple: generate one nonce per request, forward it through the request headers, build the CSP from that value, and reuse the same nonce for every allowed script. Once those values come from the same request-scoped source, CSP with nonce works reliably in Next.js.

Leave a Reply

Your email address will not be published. Required fields are marked *