Repository navigation
Description
Summary
PPR's axis-1 safety-gate for a per-request CSP nonce only accounts for a nonce produced by the createRouter({ nonce }) provider option. A per-request nonce supplied via the nonce ContextVar token (ctx.set(nonce, value) in middleware) is rendered into the document by useNonce() (Scripts/MetaTags) but is invisible to the PPR gate. As a result a ppr route still enters capture and bakes the capture request's nonce into the shared shell; on a HIT that frozen nonce is served to every request.
This contradicts the documented guarantee (skills/ppr → "What always stays on axis 1": "per-request CSP nonce"). The guarantee holds for provider-supplied nonces only.
Provenance (code refs, packages/rangojs-router/src)
- Gate:
rsc/rsc-rendering.ts:110—nonce === undefinedis one of the PPR entry conditions;nonceis a threaded param ofhandleRscRendering/handleRscRenderingInner(:53,:80). noncesource is the provider only:rsc/handler.ts:155destructuresnonce: nonceProviderfromcreateRouteroptions;:362-366setslet nonceonlyif (nonceProvider);:383-385then sets both thenonceTokenContextVar andvariables.nonce; the value is threaded down tohandleRscRendering(… nonce)at:1038.- Token path bypasses the gate: an app that does not pass
createRouter({ nonce })but instead sets the token directly in middleware —ctx.set(nonce, perRequestValue)— hasnonceProvider === undefined, so the gate'snoncestaysundefinedand PPR proceeds. The token nonce is still rendered into the HTML viauseNonce()(handles/Scripts.tsx:158,handles/MetaTags.tsx:201), bakingnonce="…"into every nonced<script>/<style>/meta tag.
Repro (minimal)
createRouter({ document, urls, cache })— nononceoption.- Middleware sets a per-request nonce via the token:
ctx.set(nonce, crypto.randomUUID())(or mirror a per-request CSP nonce). path("/", Page, { name: "home", ppr: true }, () => [loading(<Skeleton/>)]), shell material in a layout.GET /withAccept: text/html.
Observed: x-rango-shell: MISS and a background capture runs despite the per-request nonce; on a warm HIT the served shell carries the capture-time nonce, byte-identical across distinct requests.
Observed in a real app
@rangojs/[email protected], React 19, Cloudflare Workers (vite RSC dev), CFCacheStore. The app supplies its per-request CSP nonce via the token (ctx.set(nonce, cspNonce) in global middleware), not via createRouter({ nonce }). With that nonce ON — ~850 nonce="…" attributes present in the rendered HTML — an Accept: text/html GET to a ppr: true route still enters capture (x-rango-shell: MISS). The gate did not treat the token nonce as blocking.
(That route never reaches HIT in this app only because it also lacks a route-level loading() boundary — a separate, correctly-warned "structural negative" condition. That warning masks the nonce problem: once a loading() hole is added to make the route capturable, the shared-frozen-nonce behavior surfaces.)
Impact
The "per-request CSP nonce always stays on axis 1" invariant does not hold for token-supplied nonces — they fall through to the "residual hazard: middleware-derived per-user state photographed into the shared shell" path silently. With a strict per-request CSP response header, HIT responses serve a stale nonce to every visitor → the browser's CSP blocks the nonced inline scripts/styles for all but the capture request.
Suggested fix (one of)
- Make the gate see the token nonce — in the PPR entry condition, also treat a token-set nonce as "nonce present" (read
nonceTokenfrom the request variables, not just the provider-derivednonce) and fall to axis 1 when set. - Warn once — when a
pprroute's capture render emits a nonce that was not declared viacreateRouter({ nonce })(declared-intent-cannot-be-honored, mirroring the missing-shell-familywarnShellStoreMissingOnce). - Document that per-request nonces MUST be supplied via
createRouter({ nonce })for the PPR safety-gate to apply, and that the token path is unsafe underppr.
Environment
@rangojs/[email protected]- React 19, vite RSC (
cloudflaredev),CFCacheStore
Activity
fix(router): PPR gate honors a token-set per-request nonce (issue #656)
fix(router): PPR gate honors a token-set per-request nonce (issue #656)
fix(router): PPR gate honors a token-set per-request nonce (issue #656)…