Skip to content

Document the CSP nonce model across client-router soft navigations #1054

Description

@vivek7405

Problem

The docs explain CSP setup (the webjs.csp config, the per-request nonce, cspNonce() for your own inline scripts) but say nothing about how the nonce behaves across a CLIENT-ROUTER SOFT NAVIGATION. That is the one genuinely subtle, security-relevant part of the story, and the exact "Turbo Drive nonce mismatch" that trips up developers on Rails/Turbo and Next. A user setting up strict CSP has no way, from the docs, to understand why a WebJs app does not break on soft nav, and might "fix" a non-problem (for example try to thread a per-request nonce into the client).

Two things are undocumented:

  1. Enforcement vs carrier. WebJs enforces CSP via the HTTP Content-Security-Policy HEADER (so report-uri / frame-ancestors / report-only all work), and uses <meta name="csp-nonce"> purely as a client-side nonce CARRIER. It is NOT the flawed <meta http-equiv="Content-Security-Policy"> enforcement pattern. This distinction is what makes the meta-tag approach sound here.
  2. Soft-nav nonce preservation. On a soft nav the client router keeps the ORIGINAL page-load nonce and re-stamps every dynamically-inserted script/preload with it, because the browser enforces CSP against the original document's header, not the fetched response's fresh header. This was hardened across all swap paths in fix: view-transition soft-nav meta leak and stuck Suspense skeleton #1049 (reconcileHeadMetas) and fix: preserve csp-nonce on mergeHead, warn on dropped streamed resolve #1052 (mergeHead).

Design / approach

Add a short "CSP and the client router" subsection to the security docs page (and a sentence to the built-ins reference), explaining: (a) enforcement is header-based, the meta is a nonce carrier; (b) on a soft nav the active document's original nonce is authoritative, and the router re-stamps dynamically-inserted scripts/preloads with it via getCspNonce(); (c) the server still mints a fresh nonce per request (including soft-nav responses), but the browser ignores a fetched response's CSP header for the live document, so the client owns "original nonce wins". Note this is the client-side analog of the Rails HTTP_X_TURBO_NONCE server-side workaround, and needs no user configuration.

Keep it accurate to the code, do NOT overstate: the meta is name=csp-nonce (carrier), the header is the enforcer.

Implementation notes (for the implementing agent)

  • Where to edit:
    • docs/app/docs/security/page.ts, the "Content-Security-Policy (opt-in, nonce-based)" section (around L47-49). Add a "CSP and the client router" subsection after it.
    • .agents/skills/webjs/references/built-ins.md, the "### CSP (opt-in, nonce)" section (around L135), add a sentence on soft-nav nonce preservation.
  • Grounding (cite these, do not re-derive): the enforcement header is built in packages/server/src/csp.js (buildCspHeader, cspHeaderName, mintNonce); the carrier meta is emitted in packages/server/src/ssr.js (around L1744, <meta name="csp-nonce" content=...>); the client reads it in packages/core/src/router-client.js getCspNonce() and re-stamps via cloneScriptWithCorrectNonce / cloneElementWithCorrectNonce; body scripts via reactivateScripts (around L4207); the head merges preserve the meta via the csp-nonce carve-outs added in fix: view-transition soft-nav meta leak and stuck Suspense skeleton #1049 (reconcileHeadMetas) and fix: preserve csp-nonce on mergeHead, warn on dropped streamed resolve #1052 (mergeHead); CSP pages are excluded from the HTML cache in packages/server/src/html-cache.js (if (guards.cspEnabled) return false).
  • Landmines: do not claim a WebJs app uses a <meta http-equiv> CSP (it does not); do not imply the soft-nav response's CSP header applies to the live document (it does not, that is the whole point). Keep the prose within invariant 11 (no em-dashes, no semicolon-as-pause; the block-prose-punctuation hook enforces this on new lines).
  • Invariants: docs-only change, no code. Follow the docs-site style (plain HTML in an html template, <h2> / <h3> / <p> / <code>). No sidebar entry needed (it is a subsection of an existing page).
  • Tests + docs surfaces: no test layer (docs-only). Both surfaces above must stay consistent; the docs-site /llms.txt regenerates automatically from the page.

Acceptance criteria

  • docs/app/docs/security/page.ts has a "CSP and the client router" subsection covering enforcement-vs-carrier and soft-nav nonce preservation
  • built-ins.md CSP section mentions the soft-nav nonce behavior
  • The prose is accurate to the code (the header enforces, name=csp-nonce carries, the original nonce wins on soft nav) and cites no behavior a WebJs app does not have
  • No em-dashes or banned pause punctuation (invariant 11)

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

Type

No type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions