Setting CSP with Per-Request Nonces in Next.js Middleware

This guide is part of the Vercel & Next.js Security Header Management reference. A nonce-based Content-Security-Policy is the only robust way to ship a strict, 'unsafe-inline'-free policy in Next.js, because the framework injects its own inline bootstrap scripts. A nonce must be unique per response, so it cannot come from next.config.js or vercel.json — both run at build time with static values. The mechanism that runs on every request is middleware.ts. This page shows how to mint the nonce, attach it to both the request and the response CSP, read it during render, and apply it to your script tags. The directive theory is covered in the CSP nonce generation reference.

Configuration Syntax & Exact Values

The flow has four moving parts: generate a random nonce, write it into the Content-Security-Policy header, forward it to the renderer through a request header, and read it back during render to stamp onto every <Script>.

The header value you emit per request:

Content-Security-Policy: default-src 'self'; script-src 'nonce-AbC123==' 'strict-dynamic'; style-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

Annotated breakdown of the script-src tokens:

The nonce is generated with the Web Crypto API (available in the Edge runtime middleware runs in):

// 16 random bytes, base64-encoded — fresh per request.
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');

Server-Side Configuration

middleware.ts

Place middleware.ts at the project root (or src/). It generates the nonce, injects it into the outgoing CSP, and forwards it to the renderer via a custom request header so the layout can read it.

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

export function middleware(request: NextRequest) {
  // 1. Mint a fresh nonce for this single request.
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64');

  // 2. Build the policy. Newlines are collapsed to single spaces below.
  const csp = [
    "default-src 'self'",
    `script-src 'nonce-${nonce}' 'strict-dynamic'`,
    "style-src 'self'",
    "object-src 'none'",
    "base-uri 'self'",
    "frame-ancestors 'none'",
  ].join('; ');

  // 3. Forward the nonce to the renderer on a REQUEST header.
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-nonce', nonce);
  requestHeaders.set('Content-Security-Policy', csp);

  // 4. Emit the CSP on the RESPONSE so the browser enforces it.
  const response = NextResponse.next({
    request: { headers: requestHeaders },
  });
  response.headers.set('Content-Security-Policy', csp);
  return response;
}

// 5. Skip static assets so they are not needlessly processed.
export const config = {
  matcher: [
    {
      source: '/((?!_next/static|_next/image|favicon.ico).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
  ],
};

The nonce travels two ways: on the request header x-nonce (so server components can read it) and inside the response Content-Security-Policy (so the browser enforces it). They must carry the same value.

Reading the nonce in an App Router layout

In the App Router, read the forwarded x-nonce request header with headers() and pass it to every <Script>. Reading headers() opts the route into dynamic rendering.

// 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') ?? '';

  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://cdn.example.com/analytics.js"
          strategy="afterInteractive"
          nonce={nonce}
        />
      </body>
    </html>
  );
}

Next.js automatically propagates the nonce to its own framework <script> tags when it detects a nonce in the CSP, so the bootstrap scripts continue to run under a strict policy without manual tagging.

Pages Router (_document) approach

The Pages Router cannot read request headers in _document the way the App Router can. Read the nonce from the response header in getServerSideProps (or _app) and pass it down, or set it on <NextScript nonce={...}> in a custom _document using the value middleware placed on the request. The simplest robust form reads it in _document.getInitialProps:

// pages/_document.tsx
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);
    // Middleware set x-nonce on the request headers.
    const nonce = (ctx.req?.headers['x-nonce'] as string) ?? '';
    return { ...initialProps, nonce };
  }

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

export default MyDocument;
Per-request nonce flow from middleware to browser validation Middleware generates a nonce, sets it on the response CSP header and forwards it to the renderer, which stamps it on script tags that the browser then validates. middleware.ts mint nonce CSP header x-nonce req response header nonce-AbC123 layout reads <Script nonce> browser match nonce? run : block
Middleware mints the nonce, emits it on the response CSP and forwards it to the renderer; the browser runs only scripts whose nonce attribute matches the header.

Diagnostic & Verification Steps

The defining property of a correct setup is that the nonce changes on every request. Two successive curls must show two different nonces.

# Fetch the CSP twice; the nonce token must differ between calls.
curl -sI https://your-app.vercel.app/ | grep -i 'content-security-policy'
curl -sI https://your-app.vercel.app/ | grep -i 'content-security-policy'

Expected output (note the different nonce on each line):

content-security-policy: default-src 'self'; script-src 'nonce-Y2YwM2Vk' 'strict-dynamic'; style-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
content-security-policy: default-src 'self'; script-src 'nonce-OWExZmJj' 'strict-dynamic'; style-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

If both lines show the same nonce, the route is being statically cached — the nonce was frozen at build and is no longer per-request, which is a security regression (see edge cases). Confirm the rendered HTML carries the matching attribute:

# The nonce in the header must equal the nonce on the script tags.
curl -s https://your-app.vercel.app/ | grep -o 'nonce="[^"]*"' | head -1

In DevTools, open the Console: any inline or third-party script lacking the current nonce is reported with Refused to execute inline script because it violates the following Content Security Policy directive. A correctly nonced page shows no CSP violations and all framework scripts execute.

Edge Cases, Security Implications & Safe Rollback

Frequently Asked Questions

Why can’t I set the CSP nonce in next.config.js or vercel.json?

Both run at build time with static string values and no access to the request, so they would emit one frozen nonce shared by every visitor — defeating the purpose. A nonce must be unique per response, which only middleware can produce. See the comparison of vercel.json vs next.config.js.

Why is the same nonce returned on every request?

The route is being statically cached, so the nonce was frozen at build. Reading the nonce via headers() forces dynamic rendering; if you also cache the HTML at the CDN, you re-freeze it. Set Cache-Control: no-store on nonced HTML responses and confirm the route renders dynamically.

Do I have to tag Next.js framework scripts with the nonce manually?

No. When Next.js detects a nonce in the Content-Security-Policy it propagates that nonce to its own bootstrap script tags automatically. You only need to add nonce={nonce} to your own <Script> components and any third-party tags.

What does ‘strict-dynamic’ do alongside the nonce?

It lets a trusted nonced script load further scripts without each host being allowlisted, and tells supporting browsers to ignore host allowlists and ‘unsafe-inline’ in script-src. Only nonced or hashed scripts and their descendants execute, which is what makes the policy strict.

Conclusion

A per-request nonce in middleware.ts is the correct way to run a strict, 'unsafe-inline'-free CSP in Next.js: mint the nonce with the Web Crypto API, forward it on a request header, emit it on the response CSP, and read it during render to stamp every script. Accept that nonced routes render dynamically and must not be cached. Roll out incrementally: ship the policy as Content-Security-Policy-Report-Only on a preview deploy first, confirm with two curl -sI calls that the nonce changes and that DevTools shows no violations, then switch to the enforcing header in production.