Skip to content

mergeHead replaces the csp-nonce meta on a background full-body swap #1050

Description

@vivek7405

Problem

mergeHead (the full remove+add head merge used ONLY on the background in-place full-body swap path, a snapshot restore or its revalidation) treats the live <meta name="csp-nonce"> as stale whenever the incoming response carries a different per-request nonce (which it always does on a CSP-enabled app), removes it, and appends the incoming one. That violates the contract documented on getCspNonce(): the browser enforces CSP against the nonce the ORIGINAL page load declared, so after the merge getCspNonce() returns a nonce the active policy does not allow, and every later cloneScriptWithCorrectNonce / cloneElementWithCorrectNonce-stamped script or modulepreload is blocked.

This is the same bug class PR #1049 fixed for the boundary/frame paths (reconcileHeadMetas excludes META_KEY_CSP_NONCE), found while reviewing that PR. mergeHead predates #1049 and was not touched by it, so this is a pre-existing gap on the background path, filed separately.

Reach is narrow: CSP opt-in (webjs.csp) + a popstate snapshot restore's background revalidation (the only mergeHead caller) + a later dynamically-inserted nonce-stamped element. But when it hits, scripts silently stop executing until a hard reload.

Design / approach

Give mergeHead the same csp-nonce carve-out reconcileHeadMetas has: skip meta[name="csp-nonce"] in BOTH its removal loop and its add loop (the incoming per-request nonce is useless client-side anyway, since the enforced header is the original page load's). META_KEY_CSP_NONCE / metaIdentity already exist in the same file, so the check can reuse them or just match the name attribute directly.

Implementation notes (for the implementing agent)

  • Where to edit: packages/core/src/router-client.js mergeHead (~L3895). The removal loop (~L3915-3928) and the add loop (~L3930-3943) both need the skip. META_KEY_CSP_NONCE + metaIdentity are defined ~L3741-3770.
  • Landmine: the existing test mergeHead: applies meta csp-nonce to created scripts (replaces source nonce) (packages/core/test/routing/router-client.test.js ~L942) uses the SAME nonce on both sides, so it passes today and does not cover this. Do not weaken it; add the differing-nonce case alongside it.
  • The counterfactual: live head <meta name="csp-nonce" content="ORIGINAL">, incoming head with content="INCOMING", run _mergeHead, assert exactly one csp-nonce meta remains with ORIGINAL content. Fails before the fix (the meta is replaced).
  • Invariants: .js + JSDoc only in packages/; dogfood: CSS drops on client-router soft nav on real Android Chrome (styled on refresh) #936 stylesheet preservation in mergeHead must stay untouched.
  • Tests: node-level in router-client.test.js (the _merge harness export already exists). No browser layer strictly needed (pure head-merge logic, same as the existing mergeHead tests), but confirm nothing in the CSP browser suites asserts the opposite.

Acceptance criteria

  • After mergeHead with a differing incoming nonce, the live csp-nonce meta keeps the ORIGINAL content and no duplicate exists
  • Scripts created after the merge get the original page-load nonce via getCspNonce()
  • The existing same-nonce test still passes; a differing-nonce counterfactual fails when the fix is reverted

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions