Skip to content

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

Open
tomohiro86 wants to merge 1 commit intovercel:canaryfrom
tomohiro86:fix/dev-mode-xss-bootstrap-script-content
Open

fix(app-render): HTML-escape bootstrapScriptContent to prevent dev-mode XSS#93733
tomohiro86 wants to merge 1 commit intovercel:canaryfrom
tomohiro86:fix/dev-mode-xss-bootstrap-script-content

Conversation

@tomohiro86
Copy link
Copy Markdown

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: #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.

The `x-nextjs-request-id` header value was embedded into an inline
<script> tag using `JSON.stringify()` alone, which does not escape
`</script>`. An attacker with access to the dev server could send a
crafted header to break out of the script tag and execute arbitrary JS.

The same class of bug was fixed in `stream-ops.node.ts` and
`use-flight-response.tsx` in the GHSA security patch (647d923),
but the `bootstrapScriptContent` assignment in `app-render.tsx` was
missed. Applying `htmlEscapeJsonString()` consistently closes the gap.

Only affects the development server (`process.env.__NEXT_DEV_SERVER`);
production builds are not impacted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant