Skip to content

fix: add integrity attributes to inline flight scripts when experimental.sri is enabled#95696

Open
ashusnapx wants to merge 2 commits into
vercel:canaryfrom
ashusnapx:fix/sri-inline-flight-scripts
Open

fix: add integrity attributes to inline flight scripts when experimental.sri is enabled#95696
ashusnapx wants to merge 2 commits into
vercel:canaryfrom
ashusnapx:fix/sri-inline-flight-scripts

Conversation

@ashusnapx

Copy link
Copy Markdown

What

When experimental.sri is configured, the SubresourceIntegrityPlugin adds integrity hashes to external <script src> tags, but inline flight scripts (self.__next_f.push) emitted during SSR streaming had no integrity attributes. This made it impossible to use strict CSP without 'unsafe-inline'.

This PR threads the SRI algorithm through the entire SSR streaming pipeline so that every inline <script> tag containing Flight hydration data receives a content-addressed integrity attribute.

Why

With strict CSP (script-src 'self' without 'unsafe-inline'), browsers block inline scripts unless they carry a valid integrity hash. The existing experimental.sri feature only handled external scripts — all inline Flight data scripts were blocked, breaking hydration entirely under strict CSP.

Fixes #95354

How

1. Core implementation — compute integrity at emit time

Inline Flight scripts are generated at request time during SSR streaming (not at build time), so their content is unknown until render. We compute the SHA hash of each script's text content at the moment it is emitted:

  • use-flight-response.tsx — Added computeInlineScriptIntegrity() using the Web Crypto API (crypto.subtle.digest), available in both Edge and Node.js runtimes. This avoids bundling the Node.js crypto module which is unavailable in Edge/ESM builds. Updated createInlinedDataReadableStream, writeInitialInstructions, and writeFlightDataInstruction to accept sriAlgorithm and emit integrity attributes.

  • stream-ops.node.ts — Added computeInlineScriptIntegritySync() using Node.js crypto.createHash for the synchronous Node.js stream path. Updated createNodeInlinedDataStream, createWebInlinedDataStream, and pullFlightData to accept and forward sriAlgorithm.

  • stream-ops.web.ts — Updated stream function signatures to accept sriAlgorithm.

  • app-render.tsx — Extracted sriAlgorithm from renderOpts.experimental and threaded it to all 13+ createInlinedDataStream call sites across the dynamic render, PPR, static prerender, and error recovery paths.

2. Config threading — the critical fix

This was the hardest bug to find. We initially added sriAlgorithm to base-server.ts's renderOpts.experimental (line 602) and saw it correctly threaded through app-render.tsx. However, the tests still failed with scriptsWithIntegrity === 0.

Root cause: In production (next start), app pages are NOT served via base-server.ts's renderOpts. Instead, the request flows through:

  1. NextNodeServer.renderPageComponent()base-server.ts:renderPageComponent()NextNodeServer.renderHTML()lazyRenderAppPage()
  2. This invokes the handler() function in the app-page.ts build template, which constructs its own renderOpts.experimental from scratch — completely bypassing base-server.ts's setting.

The same issue exists for edge runtime pages via edge-ssr-app.ts.

Fix: Added sriAlgorithm: nextConfig.experimental.sri?.algorithm to the experimental object in:

  • packages/next/src/build/templates/app-page.ts — production next start for Node.js app pages
  • packages/next/src/build/templates/edge-ssr-app.ts — production next start for Edge runtime app pages

3. Web Crypto API algorithm name mapping

Edge runtime's crypto.subtle.digest() requires the Web Crypto algorithm format (SHA-256, SHA-384, SHA-512), but the SRI config uses lowercase without hyphens (sha256, sha384, sha512). Added a mapping step:

const webCryptoAlgorithm = algorithm.toUpperCase().replace(/^(\D+)(\d+)$/, '$1-$2')

4. Test coverage

Added a new test case includes integrity attributes on inline flight scripts to subresource-integrity.test.ts that:

  • Fetches the rendered page HTML
  • Finds all inline <script> tags (without src)
  • Verifies each has an integrity attribute in the format sha256-<base64>
  • Recomputes the SHA-256 hash over the script content and asserts it matches
  • Runs for all three runtimes: node, edge, and pages

Final test results: 21/21 passing (7 per runtime × 3 runtimes).

Files changed

File Change
packages/next/src/server/app-render/use-flight-response.tsx Core: computeInlineScriptIntegrity() + updated stream functions
packages/next/src/server/app-render/stream-ops.node.ts Core: computeInlineScriptIntegritySync() + updated node stream functions
packages/next/src/server/app-render/stream-ops.web.ts Updated web stream function signatures
packages/next/src/server/app-render/app-render.tsx Threaded sriAlgorithm to all inline data stream creation sites
packages/next/src/server/app-render/types.ts Added sriAlgorithm? to RenderOptsPartial.experimental
packages/next/src/server/base-server.ts Added sriAlgorithm to this.renderOpts.experimental
packages/next/src/build/templates/app-page.ts Key fix: Added sriAlgorithm to build template's experimental
packages/next/src/build/templates/edge-ssr-app.ts Key fix: Added sriAlgorithm to edge build template's experimental
test/production/app-dir/subresource-integrity/subresource-integrity.test.ts New test for inline script integrity

Challenges encountered during development

  1. require('crypto') breaks Edge/ESM builds — The Node.js crypto module cannot be require()'d in use-flight-response.tsx because it is bundled for Edge/ESM paths where crypto does not exist. Fixed by using the Web Crypto API exclusively.

  2. Optional sriAlgorithm? type — Initially added sriAlgorithm as required, which caused TypeScript errors in app-page.ts, edge-ssr-app.ts, and export/index.ts where not all experimental fields were being passed. Fixed by making it optional (sriAlgorithm?).

  3. base-server.ts is a dead path for app pages — The most time-consuming issue. base-server.ts:602 correctly sets sriAlgorithm on this.renderOpts.experimental, but in production, the app-page.ts build template constructs its own renderOpts from nextConfig directly, never reading base-server.ts's value. Same for edge-ssr-app.ts and edge runtime pages. This required deep tracing through the entire request pipeline: NextNodeServer.renderPageComponent()base-server.ts:renderPageComponent()renderToResponseWithComponents()renderHTML()lazyRenderAppPage()handler() in the build template.

  4. Edge runtime crypto.subtle.digest algorithm format — The Web Crypto API requires SHA-256 format while SRI uses sha256. The edge runtime threw Unrecognized algorithm name on the first pass. Fixed with a mapping regex.

  5. app-route.ts and export/index.ts are not needed — App routes (API routes) do not render HTML with inline Flight scripts, and export/index.ts is used for static export which does not generate inline scripts at request time. These do not need sriAlgorithm.

…tal.sri is enabled

When experimental.sri is configured, the SubresourceIntegrityPlugin adds
integrity hashes to external <script src> tags, but inline flight scripts
(self.__next_f.push) emitted during SSR streaming had no integrity
attributes. This made it impossible to use strict CSP without
'unsafe-inline'.

Thread sriAlgorithm through the entire SSR streaming pipeline:
- Compute SHA-256/384/512 content hashes of each inline script at emit time
- Use Web Crypto API (crypto.subtle.digest) for edge/runtime compatibility
- Use Node.js crypto.createHash for the sync node stream path
- Map SRI algorithm names to Web Crypto format (sha256 -> SHA-256)

Fix the config threading that was missing in build templates:
- app-page.ts: the production next-start build template constructs its own
  renderOpts.experimental from nextConfig, bypassing base-server.ts
- edge-ssr-app.ts: same issue for edge runtime app pages

Add comprehensive test coverage verifying integrity attributes on inline
flight scripts for node, edge, and pages runtimes.

Fixes vercel#95354
Comment thread packages/next/src/server/app-render/use-flight-response.tsx Outdated
@ashusnapx

Copy link
Copy Markdown
Author

cc @timneutkens @unstubbable @gnoff @eps1lon @lubieowoce @ztanner — touching the core app-render pipeline, SRI, flight streaming, and build templates. Would appreciate a review!

…forget in ReadableStream.start()

ReadableStream.start() is synchronous and cannot await async operations.
When sriAlgorithm is enabled, writeInitialInstructions calls
computeInlineScriptIntegrity which uses crypto.subtle.digest (async).
Previously this was fire-and-forget in start(), swallowing errors and
allowing flight data scripts to be enqueued before the bootstrap.

Move initial instructions to the first pull() call which is async,
ensuring proper ordering and error propagation.
@ashusnapx

Copy link
Copy Markdown
Author

Good catch! Fixed — moved writeInitialInstructions from ReadableStream.start() (synchronous, fire-and-forget) into the first pull() call (async). This ensures:

  1. The integrity hash computation (via crypto.subtle.digest) is properly awaited
  2. Errors are caught and forwarded to the stream, not swallowed
  3. The bootstrap script is always enqueued before any flight data scripts

All 21 tests still pass after this change.

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.

experimental.sri does not add integrity to inline flight scripts (self.__next_f.push), breaking strict CSP without 'unsafe-inline'

1 participant