`, an attacker with network access to the dev server can break out of the script tag and execute arbitrary JavaScript. This is the same class of bug that was fixed in `stream-ops.node.ts` and `use-flight-response.tsx` by the recent GHSA security patch (commit `647d923a3f`), but the `bootstrapScriptContent` assignment in `app-render.tsx` was missed. # PR #93733: fix(app-render): HTML-escape bootstrapScriptContent to prevent dev-mode XSS - Repository: vercel/next.js - Author: tomohiro86 - State: open | merged: False - Link: https://github.com/vercel/next.js/pull/93733 ## Description (problem / solution / changelog) ## What? Applies `htmlEscapeJsonString()` to the `bootstrapScriptContent` inline script generated in development server mode, closing a reflected XSS vector via the `x-nextjs-request-id` request header. ## Why? The GHSA security patch (commit `647d923a3f`) fixed the same class of bug — `JSON.stringify()` without HTML escaping in an inline ` ``` produces a broken script tag in the HTML output, allowing arbitrary JS execution. Only the dev server is affected — production builds never reach this branch (`process.env.__NEXT_DEV_SERVER` is undefined). Related issue: https://github.com/vercel/next.js/issues/93732 ## How? ```diff + import { htmlEscapeJsonString } from '../../shared/lib/htmlescape' const bootstrapScriptContent = process.env.__NEXT_DEV_SERVER - ? `self.__next_r=${JSON.stringify(requestId ?? crypto.randomUUID())}` + ? `self.__next_r=${htmlEscapeJsonString(JSON.stringify(requestId ?? crypto.randomUUID()))}` : undefined ``` A unit test is added at `packages/next/src/server/app-render/bootstrap-script-content.test.ts` verifying that a malicious request ID containing `` is safely escaped and round-trips correctly via `JSON.parse`. ## Changed files - `packages/next/src/server/app-render/app-render.tsx` (modified, +2/-1) - `packages/next/src/server/app-render/bootstrap-script-content.test.ts` (added, +35/-0) ## Fix Wrap with `htmlEscapeJsonString`, consistent with the existing fix applied to the two other locations in the GHSA patch: ```diff - ? `self.__next_r=${JSON.stringify(requestId ?? crypto.randomUUID())}` + ? `self.__next_r=${htmlEscapeJsonString(JSON.stringify(requestId ?? crypto.randomUUID()))}` ``` A fix with a regression test is available in this PR: https://github.com/tomohiro86/next.js/pull/new/fix/dev-mode-xss-bootstrap-script-content ## Summary The `x-nextjs-request-id` request header value is embedded into an inline ``, an attacker with network access to the dev server can break out of the script tag and execute arbitrary JavaScript. This is the same class of bug that was fixed in `stream-ops.node.ts` and `use-flight-response.tsx` by the recent GHSA security patch (commit `647d923a3f`), but the `bootstrapScriptContent` assignment in `app-render.tsx` was missed. ## Affected file `packages/next/src/server/app-render/app-render.tsx` — `renderToStream` function ```typescript // Current code (vulnerable) const bootstrapScriptContent = process.env.__NEXT_DEV_SERVER ? `self.__next_r=${JSON.stringify(requestId ?? crypto.randomUUID())}` : undefined ``` `requestId` is read directly from the `x-nextjs-request-id` header, which is not in the `INTERNAL_HEADERS` strip-list. ## Proof of concept Send the following request to a running dev server: ``` GET / HTTP/1.1 Host: localhost:3000 X-Nextjs-Request-Id: 0 ``` The server renders: ```html \" ``` The HTML parser closes ``, then fires the `onerror` handler. ## Impact - **Scope**: Development server only (`process.env.__NEXT_DEV_SERVER`). Production builds are not affected. - **Risk in practice**: Elevated in cloud dev environments (GitHub Codespaces, Gitpod) where the port is publicly","inLanguage":"en-US","datePublished":"2026-05-10T05:26:47Z","dateModified":"2026-05-10T05:26:47Z","mainEntityOfPage":{"@type":"WebPage","@id":"https://www.stepcodex.com/en/issue/security-dev-mode-xss-via-unescaped"},"author":{"@type":"Person","name":"tomohiro86","url":"https://github.com/tomohiro86","image":"https://github.com/tomohiro86"},"publisher":{"@type":"Organization","name":"StepCodex","url":"https://www.stepcodex.com"},"articleSection":"nextjs","about":[{"@type":"Thing","name":"nextjs","url":"https://www.stepcodex.com/en/category/nextjs"}],"contributor":[{"@type":"Person","name":"github-actions[bot]","url":"https://github.com/github-actions%5Bbot%5D","image":"https://github.com/github-actions%5Bbot%5D"}],"keywords":"Security: dev-mode XSS via unescaped x-nextjs-request-id in bootstrapScriptContent, nextjs, how to fix, fix, troubleshooting, root cause, solution, StepCodex","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/LikeAction","userInteractionCount":0}},{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://www.stepcodex.com/en/issue"},{"@type":"ListItem","position":2,"name":"nextjs","item":"https://www.stepcodex.com/en/category/nextjs"},{"@type":"ListItem","position":3,"name":"Security: dev-mode XSS via unescaped x-nextjs-request-id in bootstrapScriptContent","item":"https://www.stepcodex.com/en/issue/security-dev-mode-xss-via-unescaped"}]}]

nextjs - ✅(Solved) Fix Security: dev-mode XSS via unescaped x-nextjs-request-id in bootstrapScriptContent [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

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#93732Fetched 2026-05-11 03:12:40
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Timeline (top)
closed ×1commented ×1cross-referenced ×1labeled ×1

The x-nextjs-request-id request header value is embedded into an inline <script> bootstrap tag in development server mode using JSON.stringify() without HTML escaping. Because JSON.stringify does not escape </script>, an attacker with network access to the dev server can break out of the script tag and execute arbitrary JavaScript.

This is the same class of bug that was fixed in stream-ops.node.ts and use-flight-response.tsx by the recent GHSA security patch (commit 647d923a3f), but the bootstrapScriptContent assignment in app-render.tsx was missed.

Root Cause

The x-nextjs-request-id request header value is embedded into an inline <script> bootstrap tag in development server mode using JSON.stringify() without HTML escaping. Because JSON.stringify does not escape </script>, an attacker with network access to the dev server can break out of the script tag and execute arbitrary JavaScript.

Fix Action

Fix

Wrap with htmlEscapeJsonString, consistent with the existing fix applied to the two other locations in the GHSA patch:

- ? `self.__next_r=${JSON.stringify(requestId ?? crypto.randomUUID())}`
+ ? `self.__next_r=${htmlEscapeJsonString(JSON.stringify(requestId ?? crypto.randomUUID()))}`

A fix with a regression test is available in this PR: https://github.com/tomohiro86/next.js/pull/new/fix/dev-mode-xss-bootstrap-script-content

PR fix notes

PR #93733: fix(app-render): HTML-escape bootstrapScriptContent to prevent dev-mode XSS

Description (problem / solution / changelog)

What?

Applies htmlEscapeJsonString() to the bootstrapScriptContent inline script generated in development server mode, closing a reflected XSS vector via the x-nextjs-request-id request header.

Why?

The GHSA security patch (commit 647d923a3f) fixed the same class of bug — JSON.stringify() without HTML escaping in an inline <script> — in two places:

  • packages/next/src/server/app-render/stream-ops.node.ts
  • packages/next/src/server/app-render/use-flight-response.tsx

The bootstrapScriptContent assignment in app-render.tsx was missed:

// Before — vulnerable
const bootstrapScriptContent = process.env.__NEXT_DEV_SERVER
  ? `self.__next_r=${JSON.stringify(requestId ?? crypto.randomUUID())}`
  : undefined

requestId comes from the x-nextjs-request-id header (not in INTERNAL_HEADERS). Sending:

X-Nextjs-Request-Id: 0</script><img src=x onerror=alert(origin)>

produces a broken script tag in the HTML output, allowing arbitrary JS execution. Only the dev server is affected — production builds never reach this branch (process.env.__NEXT_DEV_SERVER is undefined).

Related issue: https://github.com/vercel/next.js/issues/93732

How?

+ import { htmlEscapeJsonString } from '../../shared/lib/htmlescape'

  const bootstrapScriptContent = process.env.__NEXT_DEV_SERVER
-   ? `self.__next_r=${JSON.stringify(requestId ?? crypto.randomUUID())}`
+   ? `self.__next_r=${htmlEscapeJsonString(JSON.stringify(requestId ?? crypto.randomUUID()))}`
    : undefined

A unit test is added at packages/next/src/server/app-render/bootstrap-script-content.test.ts verifying that a malicious request ID containing </script> is safely escaped and round-trips correctly via JSON.parse.

<!-- NEXT_JS_LLM_PR -->

Changed files

  • packages/next/src/server/app-render/app-render.tsx (modified, +2/-1)
  • packages/next/src/server/app-render/bootstrap-script-content.test.ts (added, +35/-0)

Code Example

// Current code (vulnerable)
const bootstrapScriptContent = process.env.__NEXT_DEV_SERVER
  ? `self.__next_r=${JSON.stringify(requestId ?? crypto.randomUUID())}`
  : undefined

---

GET / HTTP/1.1
Host: localhost:3000
X-Nextjs-Request-Id: 0</script><img src=x onerror=alert(origin)>

---

<script>self.__next_r="0</script><img src=x onerror=alert(origin)>"</script>

---

- ? `self.__next_r=${JSON.stringify(requestId ?? crypto.randomUUID())}`
+ ? `self.__next_r=${htmlEscapeJsonString(JSON.stringify(requestId ?? crypto.randomUUID()))}`
RAW_BUFFERClick to expand / collapse

Summary

The x-nextjs-request-id request header value is embedded into an inline <script> bootstrap tag in development server mode using JSON.stringify() without HTML escaping. Because JSON.stringify does not escape </script>, an attacker with network access to the dev server can break out of the script tag and execute arbitrary JavaScript.

This is the same class of bug that was fixed in stream-ops.node.ts and use-flight-response.tsx by the recent GHSA security patch (commit 647d923a3f), but the bootstrapScriptContent assignment in app-render.tsx was missed.

Affected file

packages/next/src/server/app-render/app-render.tsxrenderToStream function

// Current code (vulnerable)
const bootstrapScriptContent = process.env.__NEXT_DEV_SERVER
  ? `self.__next_r=${JSON.stringify(requestId ?? crypto.randomUUID())}`
  : undefined

requestId is read directly from the x-nextjs-request-id header, which is not in the INTERNAL_HEADERS strip-list.

Proof of concept

Send the following request to a running dev server:

GET / HTTP/1.1
Host: localhost:3000
X-Nextjs-Request-Id: 0</script><img src=x onerror=alert(origin)>

The server renders:

<script>self.__next_r="0</script><img src=x onerror=alert(origin)>"</script>

The HTML parser closes <script> at the first </script>, then fires the onerror handler.

Impact

  • Scope: Development server only (process.env.__NEXT_DEV_SERVER). Production builds are not affected.
  • Risk in practice: Elevated in cloud dev environments (GitHub Codespaces, Gitpod) where the port is publicly accessible under a *.github.dev or *.gitpod.io origin — stealing cookies/tokens from that origin.

Fix

Wrap with htmlEscapeJsonString, consistent with the existing fix applied to the two other locations in the GHSA patch:

- ? `self.__next_r=${JSON.stringify(requestId ?? crypto.randomUUID())}`
+ ? `self.__next_r=${htmlEscapeJsonString(JSON.stringify(requestId ?? crypto.randomUUID()))}`

A fix with a regression test is available in this PR: https://github.com/tomohiro86/next.js/pull/new/fix/dev-mode-xss-bootstrap-script-content

Related

  • GHSA cherry-pick commit that fixed the same pattern in two other files: 647d923a3f
  • packages/next/src/server/app-render/stream-ops.node.ts (fixed)
  • packages/next/src/server/app-render/use-flight-response.tsx (fixed)

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