Skip to content

fix(document): HTML-escape NextScript.getInlineScriptSource output - #2727

Open
NathanDrake2406 wants to merge 1 commit into
cloudflare:mainfrom
NathanDrake2406:nathan/document-inline-script-escaping
Open

fix(document): HTML-escape NextScript.getInlineScriptSource output#2727
NathanDrake2406 wants to merge 1 commit into
cloudflare:mainfrom
NathanDrake2406:nathan/document-inline-script-escaping

Conversation

@NathanDrake2406

Copy link
Copy Markdown
Contributor

Problem

NextScript.getInlineScriptSource() in the next/document shim returned raw JSON.stringify(context.__NEXT_DATA__):

static getInlineScriptSource(context: Readonly<HtmlProps>): string {
  return JSON.stringify(context.__NEXT_DATA__);
}

JSON.stringify does not escape characters the HTML parser treats as significant. A string value containing </script> terminates the inline script the result is embedded in, and everything after it is parsed as HTML rather than as script content. The same applies to <, >, &, U+2028 and U+2029.

Why it matters

This method exists so a custom _document can place __NEXT_DATA__ into its own inline <script>. Next.js returns htmlEscapeJsonString(data) from every return path of the equivalent method (packages/next/src/pages/_document.tsx), so an app that relied on that escaping silently lost it here — page props or query-derived values become script injection on the app's origin.

The default renderer was never affected: pages-page-response.ts already builds its __NEXT_DATA__ tag with safeJsonStringify. Only the custom-_document path diverged.

Fix

Call safeJsonStringify — the escaper already used for the framework-generated tag — so both paths emit identical output. server/html.ts has no imports, so pulling it into the shim adds no dependency weight.

Testing

  • New test in tests/document.test.ts asserts the output contains no literal </script>, contains the escaped form, and still round-trips through JSON.parse to the original value.
  • Verified the test fails against the previous implementation and passes with the fix.
  • tests/document.test.ts + tests/safe-json.test.ts: 45 passing. Repo pre-commit full check, unit/integration suites and knip all passed.

No behavior change for apps using the default document.

The shim returned raw JSON.stringify(context.__NEXT_DATA__). JSON.stringify
does not escape characters the HTML parser treats as significant, so a string
value containing "</script>" terminates the inline script it is embedded in
and the remainder is parsed as HTML.

Next.js returns htmlEscapeJsonString(data) from every path of this method
(pages/_document.tsx). A custom _document that calls the helper and inlines
the result — the pattern the method exists for — therefore loses escaping it
would keep on Next.js, turning page props or query-derived data into script
injection.

Use safeJsonStringify, the escaper this repo already applies to the
framework-generated __NEXT_DATA__ tag in pages-page-response.ts, so both
paths produce the same output. The default renderer was unaffected.
@pkg-pr-new

pkg-pr-new Bot commented Jul 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2727
npm i https://pkg.pr.new/create-vinext-app@2727
npm i https://pkg.pr.new/@vinext/types@2727
npm i https://pkg.pr.new/vinext@2727

commit: f4ab878

@github-actions

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared f4ab878 against base 3570928 using alternating same-runner rounds. Next.js was unchanged and skipped.

1 improved · 0 regressed · 5 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 132.4 KB 132.4 KB ⚫ -0.0%
Client entry size (gzip) vinext 119.8 KB 119.8 KB ⚫ 0.0%
Dev server cold start vinext 3.10 s 3.02 s 🟢 -2.6%
Production build time vinext 3.30 s 3.28 s ⚫ -0.6%
RSC entry closure size (gzip) vinext 105.6 KB 105.6 KB ⚫ -0.0%
Server bundle size (gzip) vinext 179.8 KB 179.8 KB ⚫ -0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant