nextjs - ✅(Solved) Fix Turbopack production SSR: nonce not applied to /_next/static/chunks/* <script> tags even when x-nonce and Content-Security-Policy request headers are present [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#93094Fetched 2026-04-22 07:42:54
View on GitHub
Comments
1
Participants
2
Timeline
4
Reactions
0
Timeline (top)
closed ×1commented ×1labeled ×1locked ×1

Fix Action

Fix / Workaround

Workaround currently in use: fall back to script-src 'self' 'unsafe-inline' https: (no strict-dynamic). Page loads but loses the hardening benefit of nonce-based CSP. The runway for flipping back to strict-dynamic is kept in place (request-side x-nonce, <meta> emission, CSP request header set per Next docs) so the fix is a one-line flip once the bootstrap tag starts receiving the nonce.

PR fix notes

PR #65508: Handle nonce on Next.js injected script/link tags

Description (problem / solution / changelog)

What

Ensures nonce is added to script and link tags Next.js renders. Additional cases it now handles:

  • We already passed nonce to the React rendering, though not consistently on all cases where renderToStream is called, I'm surprised there haven't been more reports of this, but now it will pass it on all cases where React rendering is called that I could find
  • In get-layer-assets.tsx we now pass nonce to both the script and link tags
  • When calling ReactDOM.preload the nonce was missing as well, ensured that the nonce is included in that case as well.

Added a test that mimicks the reproduction by adding next/font in this case.

Fixes #64037 Closes PACK-2973

<!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: ## For Contributors ### Improving Documentation - Run `pnpm prettier-fix` to fix formatting issues before opening the PR. - Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide ### Adding or Updating Examples - The "examples guidelines" are followed from our contributing doc https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md - Make sure the linting passes by running `pnpm build && pnpm lint`. See https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md ### Fixing a bug - Related issues linked using `fixes #number` - Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ### Adding a feature - Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas) - Related issues/discussions are linked using `fixes #number` - e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) - Documentation added - Telemetry added. In case of a feature if it's used or not. - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ## For Maintainers - Minimal description (aim for explaining to someone not on the team to understand the PR) - When linking to a Slack thread, you might want to share details of the conclusion - Link both the Linear (Fixes NEXT-xxx) and the GitHub issues - Add review comments if necessary to explain to the reviewer the logic behind a change ### What? ### Why? ### How? Closes NEXT- Fixes # -->

Changed files

  • packages/next/src/server/app-render/app-render.tsx (modified, +14/-9)
  • packages/next/src/server/app-render/get-layer-assets.tsx (modified, +22/-5)
  • packages/next/src/server/app-render/rsc/preloads.ts (modified, +26/-7)
  • test/e2e/app-dir/app/app/script-nonce/with-next-font/page.js (added, +11/-0)
  • test/e2e/app-dir/app/index.test.ts (modified, +10/-0)
  • test/e2e/app-dir/app/middleware.js (modified, +10/-0)
  • test/turbopack-build-tests-manifest.json (modified, +1/-0)

Code Example

// src/proxy.ts
export async function proxy(request: NextRequest) {
  const nonce = generateNonce(); // 16 random bytes, base64
  const csp = buildCsp(nonce);   // uses `'nonce-${nonce}' 'strict-dynamic'`

  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-nonce', nonce);
  requestHeaders.set('content-security-policy', csp);

  // updateSession calls NextResponse.next({ request: { headers: requestHeaders } })
  const res = await updateSession(request, requestHeaders);

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

---

// src/app/layout.tsx
const h = await headers();
const nonce = h.get('x-nonce') ?? undefined;
return (
  <html>
    <head>
      {nonce && <meta name="csp-nonce" content={nonce} />}
    </head>
    <body>...</body>
  </html>
);

---

$ curl -s https://tracking.legacyinsights.com.br/login -D /tmp/h.out -o /tmp/b.out
$ grep -i 'content-security-policy\|x-nonce' /tmp/h.out
content-security-policy: default-src 'self'; script-src 'self' 'nonce-HSfIoYELmjgrOVsBDpqPUg==' 'strict-dynamic'
$ grep -oE '<meta[^>]*csp-nonce[^>]*>' /tmp/b.out
<meta name="csp-nonce" content="HSfIoYELmjgrOVsBDpqPUg=="/>

$ grep -cE '<script[^>]*nonce=' /tmp/b.out
0

$ grep -oE '<script[^>]{0,120}>' /tmp/b.out | head -5
<script src="/_next/static/chunks/0i6wetj06smyh.js" async="">
<script src="/_next/static/chunks/15vqu5znybt9f.js" async="">
<script src="/_next/static/chunks/14l~.r2kq13je.js" async="">
<script src="/_next/static/chunks/128aahewwf1we.js" async="">
<script src="/_next/static/chunks/turbopack-0n2.452-9mjc~.js" async="">

---

function getRequiredScripts(buildManifest, assetPrefix, crossOrigin, SRIManifest, qs, nonce, pagePath) {
  // ...
  const bootstrapScript = {
    src: '',
    crossOrigin,                //  <-- no `nonce` field here
  };
  // ...
  preinitScripts = () => {
    for (let i = 0; i < preinitScriptCommands.length; i++) {
      ReactDOM.preinit(preinitScriptCommands[i], {
        as: 'script',
        nonce,                  // <-- preinit gets the nonce
        crossOrigin,
      });
    }
  };
  return [preinitScripts, bootstrapScript];
}

---

Operating System:
  Platform: linux (production container), darwin (local build)
Binaries:
  Node: 24.x
Relevant Packages:
  next: 16.2.4 (reproduces on 16.1.4 too — not a 16.2 regression)
  react: 19.x
  react-dom: 19.x
Next.js Config:
  output: standalone
  bundler: turbopack (default on 16.x)
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

Live reproduction: https://tracking.legacyinsights.com.br/login (public route, no auth required to view SSR HTML).

Minimal config excerpt from the repository (middleware proxy.ts):

// src/proxy.ts
export async function proxy(request: NextRequest) {
  const nonce = generateNonce(); // 16 random bytes, base64
  const csp = buildCsp(nonce);   // uses `'nonce-${nonce}' 'strict-dynamic'`

  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-nonce', nonce);
  requestHeaders.set('content-security-policy', csp);

  // updateSession calls NextResponse.next({ request: { headers: requestHeaders } })
  const res = await updateSession(request, requestHeaders);

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

Layout (Server Component) confirms the nonce is visible to SSR:

// src/app/layout.tsx
const h = await headers();
const nonce = h.get('x-nonce') ?? undefined;
return (
  <html>
    <head>
      {nonce && <meta name="csp-nonce" content={nonce} />}
    </head>
    <body>...</body>
  </html>
);

To Reproduce

  1. Generate a per-request nonce in a Next.js 16 middleware (src/proxy.ts, matching Next's official CSP docs).
  2. Forward the nonce via x-nonce on the request headers passed to NextResponse.next({ request: { headers } }).
  3. Additionally set the Content-Security-Policy request header to the same CSP used on the response, so Next's get-script-nonce-from-header.js can parse it.
  4. Build the app for production with Turbopack (next build, default in 16.x).
  5. Deploy and curl the SSR HTML of any page.

Current vs. Expected behavior

Current:

The root-layout-emitted <meta name="csp-nonce" content="…"> contains the nonce — propagation through headers() works end-to-end. But every framework-emitted <script src="/_next/static/chunks/*.js" async> tag lacks a nonce attribute.

$ curl -s https://tracking.legacyinsights.com.br/login -D /tmp/h.out -o /tmp/b.out
$ grep -i 'content-security-policy\|x-nonce' /tmp/h.out
content-security-policy: default-src 'self'; script-src 'self' 'nonce-HSfIoYELmjgrOVsBDpqPUg==' 'strict-dynamic'
$ grep -oE '<meta[^>]*csp-nonce[^>]*>' /tmp/b.out
<meta name="csp-nonce" content="HSfIoYELmjgrOVsBDpqPUg=="/>

$ grep -cE '<script[^>]*nonce=' /tmp/b.out
0

$ grep -oE '<script[^>]{0,120}>' /tmp/b.out | head -5
<script src="/_next/static/chunks/0i6wetj06smyh.js" async="">
<script src="/_next/static/chunks/15vqu5znybt9f.js" async="">
<script src="/_next/static/chunks/14l~.r2kq13je.js" async="">
<script src="/_next/static/chunks/128aahewwf1we.js" async="">
<script src="/_next/static/chunks/turbopack-0n2.452-9mjc~.js" async="">

28 <script> tags on the page, 0 with a nonce attribute. With script-src 'nonce-…' 'strict-dynamic' the browser refuses every chunk and the page loads blank.

Expected:

All framework-emitted <script> tags include nonce="HSfIoYELmjgrOVsBDpqPUg==" so the CSP's strict-dynamic + nonce allowlist admits them. This matches the contract documented in the CSP guide.

Likely location

packages/next/src/server/app-render/required-scripts.ts (shipped as node_modules/next/dist/server/app-render/required-scripts.js in 16.2.4):

function getRequiredScripts(buildManifest, assetPrefix, crossOrigin, SRIManifest, qs, nonce, pagePath) {
  // ...
  const bootstrapScript = {
    src: '',
    crossOrigin,                //  <-- no `nonce` field here
  };
  // ...
  preinitScripts = () => {
    for (let i = 0; i < preinitScriptCommands.length; i++) {
      ReactDOM.preinit(preinitScriptCommands[i], {
        as: 'script',
        nonce,                  // <-- preinit gets the nonce
        crossOrigin,
      });
    }
  };
  return [preinitScripts, bootstrapScript];
}

bootstrapScript is later passed as bootstrapScripts: [bootstrapScript] into React's renderToReadableStream, but the object never receives the nonce parameter that getRequiredScripts accepts. React's bootstrap script serializer therefore emits the tag without a nonce attribute.

The dev-mode variant of this bug was fixed in #65508 (closed #64037, May 2024), but the production SSR path in 16.x still exhibits it. A follow-up comment on #64037 from the original reporter already flagged a separate site that was still un-nonced.

Provide environment information

Operating System:
  Platform: linux (production container), darwin (local build)
Binaries:
  Node: 24.x
Relevant Packages:
  next: 16.2.4 (reproduces on 16.1.4 too — not a 16.2 regression)
  react: 19.x
  react-dom: 19.x
Next.js Config:
  output: standalone
  bundler: turbopack (default on 16.x)

Which area(s) are affected?

Turbopack, SSR, CSP

Which stage(s) are affected?

next build + next start (production).

Additional context

Workaround currently in use: fall back to script-src 'self' 'unsafe-inline' https: (no strict-dynamic). Page loads but loses the hardening benefit of nonce-based CSP. The runway for flipping back to strict-dynamic is kept in place (request-side x-nonce, <meta> emission, CSP request header set per Next docs) so the fix is a one-line flip once the bootstrap tag starts receiving the nonce.

extent analysis

TL;DR

The most likely fix is to update the getRequiredScripts function in required-scripts.ts to include the nonce field in the bootstrapScript object.

Guidance

  • Review the getRequiredScripts function in required-scripts.ts to ensure it correctly sets the nonce attribute for the bootstrapScript object.
  • Verify that the nonce parameter is being passed correctly to the getRequiredScripts function.
  • Check the React's renderToReadableStream function to ensure it is correctly serializing the bootstrapScript object with the nonce attribute.
  • Consider updating to a newer version of Next.js if available, as the dev-mode variant of this bug was fixed in a previous pull request.

Example

function getRequiredScripts(buildManifest, assetPrefix, crossOrigin, SRIManifest, qs, nonce, pagePath) {
  // ...
  const bootstrapScript = {
    src: '',
    crossOrigin,
    nonce, // Add the nonce field to the bootstrapScript object
  };
  // ...
}

Notes

The provided code snippet suggests that the issue is with the getRequiredScripts function not including the nonce field in the bootstrapScript object. However, without the full codebase, it's difficult to provide a comprehensive solution. Additionally, the issue may be specific to the Turbopack bundler and SSR in Next.js 16.x.

Recommendation

Apply the workaround by updating the getRequiredScripts function to include the nonce field in the bootstrapScript object, as this is the most direct solution to the problem. This will allow the strict-dynamic CSP to work correctly with the nonce-based allowlist.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING