Skip to content

Verify and document CSP-nonce propagation for streamed RSC under a strict no-unsafe-inline policy#3934

Merged
justin808 merged 17 commits into
mainfrom
jg/3879-strict-csp-rsc-streaming
Jun 15, 2026
Merged

Verify and document CSP-nonce propagation for streamed RSC under a strict no-unsafe-inline policy#3934
justin808 merged 17 commits into
mainfrom
jg/3879-strict-csp-rsc-streaming

Conversation

@justin808

@justin808 justin808 commented Jun 11, 2026

Copy link
Copy Markdown
Member

Fixes #3879

Summary

The CSP-nonce plumbing for streamed RSC shipped in 16.4.0 (PRs 2398/2418) but had zero automated coverage under a real strict policy and no documentation. This PR proves it holds end-to-end and documents the recipe:

  • Strict CSP enforced globally in the Pro dummy app (script-src 'self' + per-request nonce, NO unsafe-inline; development-only carve-outs for webpack-dev-server/HMR/eval) via the new react_on_rails_pro/spec/dummy/config/initializers/content_security_policy.rb.
  • New Playwright E2E (react_on_rails_pro/spec/dummy/e2e-tests/strict_csp.spec.ts): loads streamed RSC pages under the enforced policy with a securitypolicyviolation listener installed before any page script, and asserts zero violations + interactive hydration (input responds, RSC payload arrays populated, console replay executed). A canary test injects a nonce-less inline script and expects it blocked, proving the zero-violation assertion is not vacuous. Runs automatically in the existing dummy-app-node-renderer-e2e-tests CI job — no workflow changes.
  • New docs page docs/pro/strict-csp.md: Rails initializer recipe, nonce flow (Rails content_security_policy_noncerailsContext.cspNonce → node renderer → injected <script nonce>), what is/isn't nonce-covered and why type="application/json" data tags need no nonce, caching caveats, troubleshooting.
  • Dummy-app fixes to stay green under the policy (each demonstrates a documented pattern): nonce the prism CDN tags (+ preload_links_header: false — preload headers can't carry nonces), replace inline onchange handlers with a nonced script, nonce the committed-stream error script, and thread railsContext.cspNonce into the Apollo __APOLLO_STATE__ tags and React Router's StaticRouterProvider hydration script.

No gem/package code changes were needed: the audit found every executable inline script in the streamed-RSC + hydration path already nonce-covered.

Nonce audit

# Emission site File:lines Executable? Nonce? Verified how
1 RSC payload array init script packages/react-on-rails-pro/src/injectRSCPayload.ts:56-61,346 Yes Yes (sanitizeNonce(cspNonce)) E2E zero-violations + curl body scan (all executable inline tags nonced)
2 RSC Flight payload chunk scripts injectRSCPayload.ts:63-65,367 Yes Yes E2E + REACT_ON_RAILS_RSC_PAYLOADS populated under enforced policy
3 RSC diagnostic script injectRSCPayload.ts:78-91,361 Yes Yes curl body scan (diagnostic emitted in local run, nonced)
4 Streamed console-replay scripts injectRSCPayload.ts:371-374 Yes Yes E2E asserts replayed [SERVER] log appears in browser console
5 Nonce attach helper injectRSCPayload.ts:48-54; accept at :123-125 Code review: every createScriptTag call threads sanitizedNonce
6 React hydration bootstrap + $RC/$RX runtime scripts packages/react-on-rails-pro/src/streamServerRenderedReactComponent.ts:118 (nonce: option), :95 (cspNonce → injectRSCPayload) Yes Yes curl body scan shows $RX script nonced; E2E hydration completes
7 Immediate-hydration component script react_on_rails/lib/react_on_rails/pro_helper.rb:27-31 Yes Yes (csp_nonce) curl body scan + E2E
8 Immediate-hydration store script pro_helper.rb:52-60 Yes Yes Code review (same pattern as #7)
9 Console replay (non-streamed + first chunk) react_on_rails/lib/react_on_rails/helper.rb:554-565 Yes Yes (id="consoleReplayLog" + nonce) curl body scan
10 Component props tag pro_helper.rb:10-21 No (type="application/json" data block — browser never executes; script-src doesn't apply) Intentionally none curl scan: 2 inert JSON tags, 0 nonced — by design
11 Rails-context tag helper.rb:584-587 No (same) Intentionally none same
12 Redux store data tag pro_helper.rb:43-46 No (same) Intentionally none code review
13 sanitizeNonce false-drop check packages/react-on-rails/src/sanitizeNonce.ts Never drops legit Rails nonces Live nonces with +, /, leading +, and == padding (e.g. +k12QYROWcdDhxenkVkpHQ==) passed through and hydrated in E2E; regex permits full base64/base64url + ={0,2}
14 h2c boundary (Rails → node renderer) rendering request body Checked in passing: nonce travels as JSON inside the rendering-request body; the E2E proves header nonce == every injected script nonce end-to-end through the renderer

Gaps found and fixed (dummy-app level, not gem): committed-stream error script (application_controller.rb), Apollo __APOLLO_STATE__ tags, React Router StaticRouterProvider hydration script — all app-authored scripts outside the gem's helpers; fixed by threading the nonce, which doubles as the documented pattern for user apps.

⚠️ Cached-nonce finding (maintainer decision needed — deliberately NOT fixed here)

  • cached_stream_react_component (and the other fragment-caching helpers) bake the populating request's nonce into cached fragments. fetch_stream_react_component (react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb:327-345) caches the full streamed chunk array — including the nonced immediate-hydration, console-replay, and RSC <script nonce=…> tags — under react_component_cache_key, which does not include the nonce; handle_stream_cache_hit replays those chunks verbatim. Verified empirically in the dummy with a memory store: a cache hit (0.04s) served nonce="Q16Z/…" inside the body while the response header carried a fresh nonce-9QNhpi2/…. Under strict CSP the browser blocks every executable inline script in the cached fragment (immediate hydration, RSC payload, console replay) — the zero-violation guarantee breaks and streamed-RSC hydration data is lost. Options include nonce-placeholder substitution at serve time, excluding nonced scripts from the cached body, or documenting the incompatibility (the docs page currently does the latter).
  • prerender_caching never serves a stale nonce but is silently defeated: its cache key digests the rendering request js code, which embeds railsContext.cspNonce, so per-request nonces make every key unique — 0% cross-request hit rate plus per-request write churn. Documented as a caveat in the docs page.
  • Side observation (UNKNOWN cause): cache hits alternated between two entries for an identical page during the evidence experiment — possible nondeterminism in the view-level stream cache key worth a maintainer glance.
  • Not a CI risk today: the dummy test env uses :null_store, and :caching-tagged specs use a fresh per-example MemoryStore with a single visit.

Test plan

  • New E2E locally: 12/12 passed (4 tests × chromium/firefox/webkit) against Rails (test env) + node renderer; final-head chromium re-run 4/4.
  • Full Pro dummy E2E (chromium): 33 passed; 3 failures reproduce identically without the CSP initializer (pre-existing local-env issues, streaming-after-navigation).
  • Pro dummy RSpec: helpers+requests 99/99; system 65 examples with 1 pre-existing failure (also fails without CSP, passes after bundle rebuild); root specs 107/107.
  • Pro rubocop (--ignore-parent-exclusion), ESLint, Prettier, script/check-docs-sidebar, script/check-pro-license-headers: all green.
  • script/ci-changes-detector origin/main: Pro dummy category → Pro lint + Pro dummy integration tests (which include the e2e job that runs the new spec).
  • Codex review (codex review --base origin/main): three passes; two findings accepted and fixed (committed-stream error script nonce; Apollo/Router demo script nonces); the cached-fragment finding deliberately triaged as report-only per batch instructions.

Codex Decision Log

  • Non-blocking: CSP scope in the dummy app
    • Decision: strict policy globally ON (all envs; dev gets webpack-dev-server/HMR/eval carve-outs); per-request SecureRandom.base64(16) nonce; nonce_directives = %w[script-src]; style-src keeps unsafe-inline.
    • Why: strongest guarantee; full dummy suites stay green (baseline runs with the initializer removed prove remaining failures are pre-existing); style-src belongs to Investigate and document CSS bundling for RSC and client components #3862.
    • Review later: scope to test env if dev-mode dummy use hits CSP friction.
  • Non-blocking: E2E tolerates exactly one violation shape — blockedURI:"eval" from the react-on-rails-rsc development Flight client (createFakeFunction calls (0, eval) for server stack frames; try/catch-guarded; production build has 0 eval call sites, verified in dist).
    • Decision: filter that one dev-only signature; everything else fails the test.
    • Why: dummy test bundles are dev-mode; the eval is React's, not ours, and degrades gracefully.
    • Review later: drop the filter if test bundles switch to production mode.
  • Non-blocking: dummy demo pages fixed to be CSP-clean (prism CDN nonce + no preload header, addEventListener instead of inline onchange, nonced Apollo/Router/committed-stream scripts).
    • Decision: fix in this PR as flagged trivially-safe, in-scope app-level changes.
    • Why: the dummy must stay functional under the policy this PR enables; each fix demonstrates the documented pattern for user apps.
    • Review later: None.
  • Non-blocking: cached-nonce behavior NOT changed; no extra Ruby request-spec for the header; no CHANGELOG entry (tests/docs/dummy only per AGENTS.md).

Labels: none — dummy-app test/docs change with no gem/package/runtime code touched; path-based CI selects the Pro dummy integration suite that runs the new E2E.

🤖 Generated with Claude Code


Note

Low Risk
Changes are documentation, test coverage, and Pro dummy-app CSP alignment only; production gem/runtime behavior is unchanged.

Overview
Adds docs/pro/strict-csp.md (linked in the Pro sidebar and llms-full-pro.txt) describing how to run streamed SSR/RSC under script-src without 'unsafe-inline', including Rails nonce setup, nonce flow through railsContext.cspNonce, what tags need nonces vs inert JSON blocks, fragment/prerender caching caveats, and troubleshooting.

Enforces that policy in the Pro dummy app via a new content_security_policy.rb initializer (strict in test/production; dev carve-outs for webpack-dev-server/HMR/eval) and updates dummy views/controllers so the app stays functional: nonced Prism CDN scripts with preload_links_header: false, javascript_tag nonce: true instead of inline onchange, nonced late-stream error redirect script with safer escaping, and cspNonce on Apollo inline state tags and StaticRouterProvider.

Adds serializeForInlineScript plus unit tests for safer JSON-in-script serialization; Playwright E2E (strict_csp.spec.ts) asserts the strict CSP header, blocks a nonce-less canary script, and verifies streamed RSC pages hydrate with no unexpected securitypolicyviolation events (tolerating dev-only Flight eval noise).

No gem/package runtime changes in this diff—the work documents and continuously verifies existing nonce plumbing.

Reviewed by Cursor Bugbot for commit 3805069. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Documentation

    • Added a Pro guide for strict Content Security Policy in streamed SSR and React Server Components, including nonce propagation, script vs inert data handling, caching caveats, and troubleshooting.
  • New Features

    • Updated streamed inline scripts and hydration payloads to carry per-request CSP nonces for compatibility with CSP environments that disallow 'unsafe-inline'.
    • Updated dummy app behaviors to avoid inline event handlers and nonce-inject third-party script tags.
  • Tests

    • Added/expanded CSP-focused Playwright E2E coverage to verify header presence and enforcement.
    • Added unit tests for safe inline-script serialization.

Merge Readiness Criteria

Current evaluation as of 2026-06-15 for head 38050696a14188ee27a9c9a53e00cbad40b59a9b.

  • Release mode: accelerated-rc, from canonical release gate Release gate: react_on_rails 17.0.0 #3823 (Agent Release Mode block verified live on 2026-06-15).
  • Current head SHA: 38050696a14188ee27a9c9a53e00cbad40b59a9b.
  • Branch freshness: current origin/main was merged into the PR branch; generated llms-full.txt/llms-full-pro.txt were verified after the merge update.
  • CI/check status: Complete for current head: 25 passing checks and 9 expected/path-selected skips; no pending or failing checks. Key passes include check-llms-full, dummy-app-node-renderer-e2e-tests, rspec-dummy-app-node-renderer, dummy-app-rspack-rsc-runtime-gate, pro-lint, build, CodeQL, claude-review, Cursor Bugbot, and CodeRabbit. mergeStateStatus is CLEAN.
  • Review-thread status: Paginated GraphQL review-thread sweep reports 42 total threads and unresolved=0 immediately before merge.
  • Review feedback triage: Prior Claude/Codex/Cursor/CodeRabbit feedback was fixed or explicitly triaged in the existing PR history. The latest update only merged main and regenerated/verified generated LLMS output; no Bump react-hooks lint to v6 and document RSC compiler boundary #3963-reserved hooks v6 / React Compiler / RSC compiler-boundary docs were touched.
  • Validation run: node script/generate-llms-full.mjs --check -> passed (llms-full.txt 1621 KiB, llms-full-pro.txt 490 KiB); bash script/generate-llms-full-test.bash -> passed in prior readiness run; git diff --check -> passed; script/check-docs-sidebar -> passed; pnpm start format.listDifferent -> passed; bin/check-links prior readiness run -> passed; OSS RuboCop prior readiness run -> passed; pre-push hook prior readiness run -> passed.
  • Label/CI decision: Labels: none. Path-selected CI includes the Pro dummy/RSC checks relevant to this docs/dummy CSP verification PR; no benchmark/full-CI expansion recommended.
  • Known residual risk: The documented cached-fragment nonce caveat remains intentionally not fixed in this PR. No current-head CI/review blocker is known.
  • Merge recommendation: Merge now. Maintainer marked Verify and document CSP-nonce propagation for streamed RSC under a strict no-unsafe-inline policy #3934 approved and explicitly authorized merge if confidence is at least 8/10; current score is 8/10.

Agent Merge Confidence

Mode: accelerated-rc
Current head SHA: 38050696a14188ee27a9c9a53e00cbad40b59a9b
Score: 8/10
Auto-merge recommendation: yes, under the explicit maintainer approval/authorization in the batch thread for #3934.
Affected areas: Pro dummy app strict CSP, streamed RSC verification, Pro CSP docs, generated LLMS references.
CI detector: prior readiness run selected Pro dummy integration/E2E and docs/LLMS checks; current-head GitHub checks completed those relevant gates.
Validation run:

  • node script/generate-llms-full.mjs --check -> pass.
  • git diff --check -> pass.
  • pnpm start format.listDifferent -> pass.
  • Current-head GitHub checks -> 25 pass / 9 expected skips / 0 pending / 0 failed.
    Review/check gate:
  • GitHub checks: complete for 38050696a14188ee27a9c9a53e00cbad40b59a9b; skips are path-selected helper skips.
  • Review threads: GraphQL unresolved count is 0.
  • Current-head reviewer verdicts: claude-review, Cursor Bugbot, CodeRabbit completed with no unresolved blocker threads.
    Known residual risk: cached-fragment nonce caveat is documented but not fixed in this PR.
    Finalized by: maintainer approval and explicit merge authorization in the Codex batch thread; live GitHub maintainer comment from justin808 says “Approved by maintainer.”

Confidence note:

  • Validated: local merge-update LLMS/format/whitespace checks, prior local docs/link/RuboCop/pre-push evidence, and current-head GitHub checks.
  • Evidence: gh pr checks 3934 -> 25 pass / 9 skip / 0 pending / 0 failed; GraphQL review threads -> 42 total / 0 unresolved; mergeStateStatus -> CLEAN.
  • UNKNOWN: none that affects merge qualification.
  • Residual risk: cached-fragment nonce behavior is documented but not fixed.
  • Decision points: 1 (maintainer approval/merge authorization).

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds strict, nonce-based CSP support for React on Rails Pro streaming and RSC: Rails initializer for per-request nonce generation, nonce propagation into server-rendered components and inline scripts, view/template updates to avoid inline handlers, inline script serialization helper, Playwright E2E tests verifying zero CSP violations, and documentation.

Changes

Strict CSP for Streaming & React Server Components

Layer / File(s) Summary
Rails CSP configuration and nonce infrastructure
react_on_rails_pro/spec/dummy/config/initializers/content_security_policy.rb
Configures a strict Content Security Policy with per-request nonce generation (content_security_policy_nonce_generator) and nonce directives limited to script-src, plus development-only allowances for webpack-dev-server and HMR.
Inline script serialization utility
react_on_rails_pro/spec/dummy/client/app/utils/serializeForInlineScript.ts, react_on_rails_pro/spec/dummy/tests/serialize-for-inline-script.test.ts
Adds a TypeScript helper that JSON-serializes values and escapes < characters for safe inline script embedding, with unit tests verifying escaping and JSON round-trip behavior.
Server-side React components with nonce propagation
react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ApolloGraphQLApp.server.jsx, react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/LazyApolloGraphQLApp.server.tsx, react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RouterApp.server.jsx
Updates server components to accept railsContext, read cspNonce, and render inline scripts with nonce attributes for Apollo state hydration and Router bootstrap.
View templates and helpers with nonce injection
react_on_rails_pro/spec/dummy/app/controllers/application_controller.rb, react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb, react_on_rails_pro/spec/dummy/app/views/pages/error_scenarios_hub.html.erb
Injects nonce into streamed redirect script, replaces hardcoded Prism includes with javascript_include_tag calls that set nonce: true, and removes inline onchange handlers in favor of a nonce-backed event listener script.
End-to-end CSP verification tests
react_on_rails_pro/spec/dummy/e2e-tests/strict_csp.spec.ts
Adds Playwright tests that capture CSP violations early via page init script, assert CSP header presence and script-src requirements ('self' and 'nonce-', no 'unsafe-inline'), confirm nonce-less scripts are blocked, validate streamed RSC hydration with zero unexpected CSP violations, and test multi-component streaming pages.
CSP documentation and sidebar registration
docs/pro/strict-csp.md, docs/sidebars.ts
Adds a guide describing the Rails initializer recipe, nonce lifecycle from Rails response header through renderer to inline scripts, which injected tags require nonces versus JSON data blocks, caching interactions (fragment and prerender), troubleshooting for common CSP failures, and registers the doc in the Pro sidebar.
Documentation exclusions and LLMS generation updates
docs/.llms-exclusions, llms-full.txt
Updates docs exclusion lists for LLMS generation and adds the new Strict CSP documentation page to llms-full.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

review-needed, documentation, full-ci, P2

Suggested reviewers

  • AbanoubGhadban
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: verification and documentation of CSP-nonce propagation for streamed RSC under strict no-unsafe-inline policy.
Linked Issues check ✅ Passed All coding objectives from #3879 are addressed: CSP initializer added, strict policy configured, E2E tests verify nonce propagation, documentation provided, nonce audit completed, and CI integration confirmed.
Out of Scope Changes check ✅ Passed All changes directly support strict CSP nonce verification and documentation objectives; no unrelated modifications detected beyond the stated scope.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg/3879-strict-csp-rsc-streaming

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a65c7571e3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@claude

claude Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Code Review — PR #3934: Strict CSP nonce verification for streamed RSC

Overview

This PR delivers exactly what it says on the tin: proof + documentation that every inline script injected by the streaming SSR / RSC pipeline carries the per-request Rails nonce, under a genuinely strict policy with no 'unsafe-inline'. No gem or package runtime code was changed — only the Pro dummy app and docs. The scope is tight and the PR body is unusually thorough with an empirically derived nonce audit table.

What's good

  • Nonce audit table in the PR body is excellent — systematically covering 14 emission sites with evidence type (E2E / curl / code review). Very useful for future maintainers auditing new script injection sites.
  • Canary test is the right design choice: proving the zero-violation assertion is non-vacuous by showing a nonce-less injection is blocked is a pattern worth keeping in mind for other security specs.
  • installCspViolationCollector via addInitScript: registering the listener at document-creation time means violations from the very first streamed chunk are captured — a subtle but important detail that's easy to get wrong.
  • Dev carve-outs properly isolated: the if Rails.env.development? branch in the initializer keeps 'unsafe-eval' / webpack-dev-server allowances out of test and production.
  • Caching caveat section in the docs is notably honest about current limitations. Documenting the fragment-cache/nonce incompatibility (rather than silently shipping broken behaviour) is the right call.
  • preload_links_header: false explanation for cross-origin nonce-tagged scripts is a non-obvious gotcha documented well in both the troubleshooting section and inline in the ERB.

Issues

1 — Minor reliability: canary test checks violations before streaming settles
See inline comment on strict_csp.spec.ts line 139.

2 — Minor security/hygiene: manual nonce interpolation in the controller
See inline comment on application_controller.rb line 56.

3 — Nit: getUnexpectedCspViolations called twice in the canary test (lines 147–148)
The helper is called once to poll until length === 1, then immediately again to inspect the violation. If a concurrent violation arrives between calls (unlikely but possible), the count shifts. The cleanest fix stores the result inside the poll callback:

let violations: CspViolation[] = [];
await expect.poll(async () => {
  violations = await getUnexpectedCspViolations(page);
  return violations.length;
}).toBe(1);
expect(violations[0].blockedURI).toBe('inline');

Not a blocker — the window is tiny and the test is correct in spirit.

Summary

Approve with minor comments. The implementation is solid, the test design is thoughtful, and the documentation is among the best in this repo for a security-sensitive feature. The two inline issues are non-blocking; the canary timing concern warrants a look if streaming latency in CI proves variable.

Comment thread react_on_rails_pro/spec/dummy/e2e-tests/strict_csp.spec.ts
Comment thread react_on_rails_pro/spec/dummy/app/controllers/application_controller.rb Outdated
@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR verifies and documents CSP-nonce propagation for streamed RSC under a strict script-src 'self' 'nonce-…' policy by adding a CSP initializer to the Pro dummy app, a Playwright E2E suite, and a new docs page. No gem or package runtime code is changed.

  • New content_security_policy.rb initializer enforces strict script-src with a per-request nonce in test/production and correctly carves out webpack-dev-server/eval only in development; combined with the new strict_csp.spec.ts E2E tests that assert zero violations plus interactive hydration (with a canary to prove the policy is genuinely enforced).
  • Dummy-app fixes (nonce Prism CDN tags with preload_links_header: false, replace inline onchange with addEventListener, add nonce to Apollo state and React Router hydration scripts) each demonstrate the documented user-land pattern.
  • Known caveats clearly documented: fragment-caching helpers bake the populating request's nonce into cached chunks (incompatible with strict CSP until addressed); prerender_caching silently becomes a no-op with per-request nonces.

Confidence Score: 4/5

Safe to merge — all changes are confined to the dummy app, docs, and a new E2E test; no gem or package runtime code is touched.

The PR is well-structured and thoroughly tested. The only non-trivial concern is in application_controller.rb where the CSP nonce is manually interpolated into an HTML attribute without HTML escaping — safe today with the default SecureRandom generator but brittle if the generator is ever swapped for a custom one. Everything else (initializer, E2E suite, Apollo/Router nonce fixes, docs) looks correct and intentional.

react_on_rails_pro/spec/dummy/app/controllers/application_controller.rb — nonce interpolation without HTML escaping is the one spot worth a second look before merging.

Important Files Changed

Filename Overview
docs/pro/strict-csp.md New documentation page covering CSP nonce flow, caching caveats, and troubleshooting — accurate, comprehensive, and includes explicit warnings for the fragment-caching incompatibility.
docs/sidebars.ts Adds strict-csp entry to the Pro sidebar at the correct position.
react_on_rails_pro/spec/dummy/app/controllers/application_controller.rb Adds per-request CSP nonce to the late-streamed error recovery script; nonce is interpolated directly into HTML without HTML escaping (safe with the default generator, fragile with a custom one).
react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb Replaces bare script tags for Prism CDN scripts with javascript_include_tag using nonce: true and preload_links_header: false — correct pattern for cross-origin scripts under strict CSP.
react_on_rails_pro/spec/dummy/app/views/pages/error_scenarios_hub.html.erb Removes inline onchange handlers and replaces with a nonced script using addEventListener — correct CSP-compatible approach.
react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ApolloGraphQLApp.server.jsx Threads railsContext.cspNonce into the Apollo state inline script tag — straightforward fix for the executable window.APOLLO_STATE assignment.
react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/LazyApolloGraphQLApp.server.tsx Same nonce threading fix as ApolloGraphQLApp.server.jsx, applied to the lazy TypeScript variant.
react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RouterApp.server.jsx Passes railsContext.cspNonce to StaticRouterProvider so React Router's inline hydration-data script carries the nonce.
react_on_rails_pro/spec/dummy/config/initializers/content_security_policy.rb New CSP initializer enforcing strict script-src with per-request nonce across all environments; dev carve-outs for webpack-dev-server/eval are correctly scoped behind Rails.env.development? guard.
react_on_rails_pro/spec/dummy/e2e-tests/strict_csp.spec.ts Well-structured Playwright suite with dev-build eval carve-out, canary validation, and hydration checks; console-replay assertion is coupled to a hardcoded log string from a specific component.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant Rails
    participant NodeRenderer

    Rails->>Rails: "Generate nonce via SecureRandom.base64(16)"
    Rails->>Browser: "Content-Security-Policy: script-src 'self' 'nonce-XYZ'"
    Rails->>NodeRenderer: "Rendering request body with railsContext.cspNonce=XYZ"
    NodeRenderer->>NodeRenderer: "renderToPipeableStream(nonce=XYZ)"
    NodeRenderer->>Browser: "React hydration bootstrap script nonce=XYZ"
    NodeRenderer->>Browser: "RSC payload init script nonce=XYZ (injectRSCPayload)"
    NodeRenderer->>Browser: "Flight payload chunk script nonce=XYZ"
    NodeRenderer->>Browser: "Console-replay script nonce=XYZ"
    Rails->>Browser: "Immediate-hydration script nonce=XYZ (pro_helper.rb)"
    Browser->>Browser: "All nonces match header - execute scripts"
    Browser->>Browser: "Hydration completes, zero CSP violations"
Loading

Comments Outside Diff (1)

  1. react_on_rails_pro/spec/dummy/e2e-tests/strict_csp.spec.ts, line 553-555 (link)

    P2 Hard-coded console-replay assertion couples the test to a specific component log string. If AsyncComponentsTreeForTesting's server-side log message is ever renamed or removed, this assertion silently becomes a false-negative (the test still passes because consoleMessages.some(...) returns false, but toBe(true) would fail). Consider extracting the expected string to a named constant at the top of the describe block so the coupling is explicit and easy to update.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "Nonce app-level inline scripts in Pro du..." | Re-trigger Greptile

Comment thread react_on_rails_pro/spec/dummy/app/controllers/application_controller.rb Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/LazyApolloGraphQLApp.server.tsx (1)

67-71: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Escape __SSR_COMPUTATION_CACHE before embedding in inline script.

Line 70 serializes cache data without escaping <. If cached content contains </script>, it can break out of the tag and become executable markup.

Suggested fix
       dangerouslySetInnerHTML={{
         __html: `
           window.__APOLLO_STATE__=${JSON.stringify(initialState).replace(/</g, '\\u003c')};
-          window.__SSR_COMPUTATION_CACHE=${JSON.stringify(getSSRCache())};
+          window.__SSR_COMPUTATION_CACHE=${JSON.stringify(getSSRCache()).replace(/</g, '\\u003c')};
         `,
       }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/LazyApolloGraphQLApp.server.tsx`
around lines 67 - 71, The inline script embeds window.__SSR_COMPUTATION_CACHE
using JSON.stringify(getSSRCache()) without escaping `<`, which can break out of
the script tag; change the injection to escape `<` characters from the
serialized cache (same approach used for initialState) before assigning to
window.__SSR_COMPUTATION_CACHE so that dangerouslySetInnerHTML uses
JSON.stringify(getSSRCache()).replace(/</g,'\\u003c') to sanitize the value
referenced in LazyApolloGraphQLApp.server.tsx (look for dangerouslySetInnerHTML,
initialState, and getSSRCache).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/LazyApolloGraphQLApp.server.tsx`:
- Around line 67-71: The inline script embeds window.__SSR_COMPUTATION_CACHE
using JSON.stringify(getSSRCache()) without escaping `<`, which can break out of
the script tag; change the injection to escape `<` characters from the
serialized cache (same approach used for initialState) before assigning to
window.__SSR_COMPUTATION_CACHE so that dangerouslySetInnerHTML uses
JSON.stringify(getSSRCache()).replace(/</g,'\\u003c') to sanitize the value
referenced in LazyApolloGraphQLApp.server.tsx (look for dangerouslySetInnerHTML,
initialState, and getSSRCache).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ca46517-6563-4c83-8e6b-38d1e40c3c43

📥 Commits

Reviewing files that changed from the base of the PR and between 661c6fa and a65c757.

📒 Files selected for processing (10)
  • docs/pro/strict-csp.md
  • docs/sidebars.ts
  • react_on_rails_pro/spec/dummy/app/controllers/application_controller.rb
  • react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb
  • react_on_rails_pro/spec/dummy/app/views/pages/error_scenarios_hub.html.erb
  • react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/ApolloGraphQLApp.server.jsx
  • react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/LazyApolloGraphQLApp.server.tsx
  • react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/RouterApp.server.jsx
  • react_on_rails_pro/spec/dummy/config/initializers/content_security_policy.rb
  • react_on_rails_pro/spec/dummy/e2e-tests/strict_csp.spec.ts

justin808 added a commit that referenced this pull request Jun 11, 2026
- strict_csp.spec.ts: wait for the streamed content (header + both
  Suspense branches at level 0) to be fully delivered before asserting
  the zero-violation baseline in the enforcement-canary test. Previously
  the baseline ran right after the load event, when streamed chunks
  could still be in transit, making the pre-canary assertion vacuous.
- application_controller.rb: HTML-escape the CSP nonce before
  interpolating it into the script tag attribute. Safe today with
  Rails' base64/session generators, but the raw interpolation was a
  copyable pattern that a custom nonce generator could break out of.

The docs page already shows only the safe helper-based patterns
(javascript_tag/javascript_include_tag with nonce: true), so no docs
change is needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f5a4140c30

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread react_on_rails_pro/spec/dummy/app/controllers/application_controller.rb Outdated
@claude

claude Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Code Review — PR #3934: Strict CSP for Streamed RSC

Overall: Good to merge. The changes are limited to the Pro dummy app, docs, and E2E tests — no gem or package runtime code is touched. The nonce plumbing is already correct in the framework; this PR proves it and documents it.


Strengths

  • Canary test is the right design. Injecting a nonce-less script and asserting it's blocked prevents the zero-violation assertion from being vacuous. This is the correct pattern for CSP E2E testing and worth calling out as a best practice.

  • ERB::Util.html_escape in the controller nonce injection (application_controller.rb:150) is correct and the comment explaining why is useful. One small note: content_security_policy_nonce is memoized per-request in Rails, so the value here will match the header — no risk of divergence.

  • nonce: true + preload_links_header: false on CDN tags is the right combination. The explanation of why preload headers can't carry nonces is valuable and matches the troubleshooting section in the docs.

  • addEventListener replacing onchange="..." is the correct CSP-clean pattern, and using el.form.submit() (rather than this.form.submit()) is intentionally correct for arrow functions.

  • Documentation is thorough. The nonce flow section, the data-block vs executable table, caching caveats, and troubleshooting section are all valuable. The per-request vs session-based nonce explanation for Turbo/Turbolinks is a subtle point many developers miss.


Issues / Suggestions

1. isDevBuildFlightClientEval source-path filter is fragile (moderate)

The filter in strict_csp.spec.ts matches on violation.sourceFile.includes('react-on-rails-rsc'). If the bundle output paths change (e.g., source maps are disabled, or the package is renamed/relocated in a future version), this filter silently stops working — either it begins allowing real violations through, or it fails to filter the expected development-mode ones. See inline comment.

2. __APOLLO_STATE__ nonce in ApolloGraphQLApp.server.jsx / LazyApolloGraphQLApp.server.tsx

railsContext.cspNonce is passed directly as nonce={railsContext.cspNonce}. This is safe because React's JSX attribute encoding handles the value. However, the __html content in dangerouslySetInnerHTML uses JSON.stringify(initialState).replace(/</g, '\\u003c') — the < replacement is correct, but since this is SSR-rendered via renderToString and then embedded in the page, the Apollo state content is treated as executable JavaScript. This is an existing pattern (not introduced by this PR), but it's worth confirming the nonce used here comes from the same request that will serve the page (i.e., there's no intermediate caching layer between renderToString and the response). The E2E tests passing confirms this in practice.

3. Cached fragment nonce issue is correctly documented — one clarification worth adding

The docs note that cached_stream_react_component bakes the nonce into the fragment. The recommendation is to avoid combining fragment caching with nonce-enforcing CSP. It would be worth adding a one-line note that this also applies to cached_react_component and cached_react_component_hash (non-streaming), not just the stream variant — the text currently lists all four helpers but the prose emphasis is on streaming. Minor clarity improvement.

4. Dev-only eval toleration — CI risk if test bundles switch to production mode

The PR notes this in the Codex decision log ("Review later: drop the filter if test bundles switch to production mode"). Consider adding a // TODO comment or a short assertion that the eval filter is only needed when NODE_ENV !== 'production' so a future bundle mode switch surfaces this clearly in the test output rather than silently passing.


Security

No new attack surface introduced. The nonce injection in application_controller.rb is correctly sanitized. The CSP initializer's development-only carve-outs (unsafe-eval, localhost:3035) are correctly scoped to Rails.env.development? and do not affect test or production environments.


Test Coverage

E2E suite covers: CSP header shape, policy enforcement (canary), zero-violation hydration, console-replay execution, RSC payload population, and multi-component stress test. This is comprehensive for a docs/demo PR.

🤖 Generated with Claude Code

Comment thread react_on_rails_pro/spec/dummy/e2e-tests/strict_csp.spec.ts Outdated
Comment thread react_on_rails_pro/spec/dummy/app/controllers/application_controller.rb Outdated
@justin808

justin808 commented Jun 13, 2026

Copy link
Copy Markdown
Member Author

Codex advisory claim update: pushed rebased head ba18e5530 with request.content_security_policy_nonce fix for the streamed rescue P2. Local validation: Pro rubocop on touched Ruby passed, script/check-pro-license-headers passed, script/check-docs-sidebar passed, pnpm start format.listDifferent passed after removing ignored Playwright .last-run.json artifact. PR is waiting on fresh current-head checks/reviews.

justin808 added a commit that referenced this pull request Jun 13, 2026
- strict_csp.spec.ts: wait for the streamed content (header + both
  Suspense branches at level 0) to be fully delivered before asserting
  the zero-violation baseline in the enforcement-canary test. Previously
  the baseline ran right after the load event, when streamed chunks
  could still be in transit, making the pre-canary assertion vacuous.
- application_controller.rb: HTML-escape the CSP nonce before
  interpolating it into the script tag attribute. Safe today with
  Rails' base64/session generators, but the raw interpolation was a
  copyable pattern that a custom nonce generator could break out of.

The docs page already shows only the safe helper-based patterns
(javascript_tag/javascript_include_tag with nonce: true), so no docs
change is needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@justin808 justin808 force-pushed the jg/3879-strict-csp-rsc-streaming branch from f5a4140 to ba18e55 Compare June 13, 2026 10:45
@claude

claude Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overview: This PR adds strict-CSP proof-of-coverage for the nonce plumbing introduced in 16.4.0 — no gem/package runtime changes, only docs, a CSP initializer for the Pro dummy app, and a new Playwright E2E suite. The scope is well-contained and the nonce audit table in the PR description is thorough.


Security

  • application_controller.rb — manually building the nonce="…" attribute via %(nonce="#{ERB::Util.html_escape(nonce)}") is the right approach for a controller-level string build. html_escape correctly prevents a malformed custom-generator value from breaking out of the attribute. ✓
  • ApolloGraphQL / LazyApollodangerouslySetInnerHTML was pre-existing; adding nonce={railsContext.cspNonce} is the correct fix. React passes nonce={undefined} gracefully (omits the attribute), so there is no risk when no nonce is configured. ✓
  • RouterApp.server.jsxStaticRouterProvider nonce={railsContext.cspNonce} is the documented React Router v6 pattern. ✓

Test Quality

The Playwright spec is well-designed on the points that matter most:

  • addInitScript to install the securitypolicyviolation listener before any page script executes — this is the correct and critical choice; a normal page.on listener registered after goto would miss violations from early streamed chunks.
  • Canary test that injects a nonce-less script and asserts it is both blocked and not executed — this proves the zero-violation assertion is not vacuous, which is the usual trap with CSP tests.
  • isDevBuildFlightClientEval filter — well-documented trade-off; the production build has no eval call sites and the filter is specific enough (blockedURI === 'eval' + source-file check) that it will not silently mask real violations.

One minor issue in the canary test — see inline comment below.


Documentation

docs/pro/strict-csp.md is excellent: nonce flow walkthrough, coverage table (with explicit "no nonce needed" justification for data-block JSON tags), caching caveats with concrete reproduction steps, and troubleshooting entries for every common failure mode. The caching section is especially important to have in writing.

One small gap: Step 2 of "How the Nonce Flows" references the helper csp_nonce parenthetically without explaining what it is or where it comes from. Linking to the RoR Pro docs for railsContext.cspNonce or spelling out that it maps to Rails' content_security_policy_nonce(:script) view helper would help adopters who search for that symbol.


Cached-nonce finding

The decision to document rather than fix the fragment-caching stale-nonce issue is the right call for this PR. The write-up in the PR description and the Caching Caveats section in the docs are clear and give adopters enough to make an informed decision.


Minor Issues

Two small items flagged with inline comments (neither is blocking).

Comment thread react_on_rails_pro/spec/dummy/e2e-tests/strict_csp.spec.ts Outdated
@justin808

Copy link
Copy Markdown
Member Author

codex-claim\nstatus: waiting_checks_review\nworker: codex\nhead: 22df7c9\nnotes: Added generated-docs fixup so llms-full stays below the split cap after the CSP/RSC docs change. Local validation: node script/generate-llms-full.mjs --check; pnpm start format.listDifferent; git diff --check origin/main..HEAD; pre-push branch RuboCop and markdown links. Waiting for rerun checks/review/finalizer.

@claude

claude Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Code Review: PR #3934 — Strict CSP nonce propagation for streamed RSC

Overall: This is a well-scoped, high-quality PR. The nonce audit table in the PR description is exemplary documentation, the E2E test design (installing the violation collector as an init script so it fires before any streamed chunk) is exactly right, and the cached-nonce finding is a valuable proactive disclosure. Scope is tightly limited to dummy app + docs — no gem/package runtime changes.


Security

LazyApolloGraphQLApp.server.tsx — missing </script> escape on SSR computation cache

The Apollo state is correctly escaped with .replace(/</g, '\\u003c') to prevent </script> from breaking out of the script tag, but getSSRCache() output is not:

window.__APOLLO_STATE__=${JSON.stringify(initialState).replace(/</g, '\\u003c')};
window.__SSR_COMPUTATION_CACHE=${JSON.stringify(getSSRCache())};  // no escape

If cached SSR computation data contains </script>, the browser parser closes the script tag early, the rest of the initialisation is silently dropped, and hydration breaks. The existing nonce prevents an injected tag from executing, but it does not protect the script that is already open. Suggest applying the same .replace(/</g, '\\u003c') to getSSRCache() too.

Inline comment posted on the relevant line.

application_controller.rb — Rails path interpolated into a JS string literal without JS encoding

server_side_log_throw_raise_invoker_path is a fixed named-route helper, so in practice it is safe. But it is interpolated into a single-quoted JavaScript string literal, meaning a ' in the path would produce a JS syntax error. The comment in the file rightly calls out the nonce attribute escaping — the same care should extend to the URL value. Suggest ActiveSupport::JSON.encode(...) rather than bare string interpolation inside a JS literal.

Inline comment posted on the relevant line.


Code Quality

strict_csp.spec.tsisDevBuildFlightClientEval filter relies on source-file path string match

The tolerated violation is identified by violation.sourceFile.includes('react-on-rails-rsc'). This will silently absorb any eval violation from any file in that package. If the dev Flight client gains additional eval call-sites that are real problems in a future version, or if the source map filename changes, the guard may over-permit or under-permit. A more resilient check would additionally cap the count of this shape (e.g. <= 3, matching the 3 known call sites documented in the PR description). Not a blocker.

docs/.llms-exclusions — "temporarily excluded" with no tracking reference

The comment reads "Excluded until llms-full is split". Consider linking to a tracking issue so this does not become permanent by default.


Documentation

The new docs/pro/strict-csp.md page is excellent. The caching-caveats section (fragment-cache stale-nonce leads to violations; prerender-cache per-request key churn silently defeats the cache) is actionable and honest about current limitations. The nonce-flow walkthrough is clear and complete.

One minor note: the Turbolinks/Turbo paragraph in the "Rails Recipe" section correctly states that hydration after a Turbo visit relies on the external client bundle + inert JSON data tags. It is worth a sentence clarifying that the initial full-page load still requires inline-script execution (RSC payload, console replay, etc.) so readers do not infer that Turbo eliminates all inline-script dependencies.


Summary

Two items warrant fixes before merge (both in dummy-app demo code, not gem/package runtime):

  1. Missing </script> escape on getSSRCache() in LazyApolloGraphQLApp.server.tsx — correctness bug, can cause silent hydration failure if SSR cache data contains </script>.
  2. Bare path interpolation into a JS string literal in application_controller.rb — defensive coding issue in security-sensitive error-handling code.

Everything else is non-blocking. The PR is well-tested and the proactive disclosure of the caching caveats is the right call.

Comment thread react_on_rails_pro/spec/dummy/app/controllers/application_controller.rb Outdated
@justin808

Copy link
Copy Markdown
Member Author

Refreshing this claim while continuing the closeout batch. Working from current origin/main with #3977 coordination present; avoiding reserved #3963 docs lane.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: faa7c0b42b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/pro/strict-csp.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
react_on_rails_pro/spec/dummy/tests/serialize-for-inline-script.test.ts (1)

18-30: ⚡ Quick win

Add a regression test for top-level undefined input.

Current coverage is strong for script-closing strings, but adding a case for serializeForInlineScript(undefined) will lock the edge behavior and prevent regressions.

Test addition
 describe('serializeForInlineScript', () => {
   it('escapes script-closing payloads while preserving JSON round trips', () => {
@@
     expect(JSON.parse(serialized)).toEqual(payload);
   });
+
+  it('handles top-level undefined safely', () => {
+    const serialized = serializeForInlineScript(undefined);
+    expect(serialized).toBe('null');
+    expect(JSON.parse(serialized)).toBeNull();
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails_pro/spec/dummy/tests/serialize-for-inline-script.test.ts`
around lines 18 - 30, The test suite for serializeForInlineScript is missing
coverage for the edge case where undefined is passed as input. Add a new test
case (using it()) within the describe('serializeForInlineScript') block that
calls serializeForInlineScript(undefined) and verifies the expected behavior for
that edge case, ensuring the function handles undefined input correctly and
consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@react_on_rails_pro/spec/dummy/tests/serialize-for-inline-script.test.ts`:
- Around line 18-30: The test suite for serializeForInlineScript is missing
coverage for the edge case where undefined is passed as input. Add a new test
case (using it()) within the describe('serializeForInlineScript') block that
calls serializeForInlineScript(undefined) and verifies the expected behavior for
that edge case, ensuring the function handles undefined input correctly and
consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d6f0e7ab-375e-430f-83cc-9431d159a3b6

📥 Commits

Reviewing files that changed from the base of the PR and between 22df7c9 and faa7c0b.

📒 Files selected for processing (4)
  • react_on_rails_pro/spec/dummy/app/controllers/application_controller.rb
  • react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/LazyApolloGraphQLApp.server.tsx
  • react_on_rails_pro/spec/dummy/client/app/utils/serializeForInlineScript.ts
  • react_on_rails_pro/spec/dummy/tests/serialize-for-inline-script.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/LazyApolloGraphQLApp.server.tsx
  • react_on_rails_pro/spec/dummy/app/controllers/application_controller.rb

@claude

claude Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Code review summary -- see inline comments for details. Overall this is a well-scoped tests-docs-dummy-app-only PR. Main actionable item: ApolloGraphQLApp.server.jsx should import and use serializeForInlineScript for consistency with LazyApolloGraphQLApp.server.tsx. Other findings are in inline comments.

Comment thread docs/pro/strict-csp.md
Comment thread react_on_rails_pro/spec/dummy/client/app/utils/serializeForInlineScript.ts Outdated
@claude

claude Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overall: Well-executed verification-and-documentation PR. The nonce audit table is thorough, the E2E test structure is solid (violation collector installed before any page script via addInitScript, canary test proves the policy is actually enforced, hydration assertions prove scripts ran), and the caching caveats are clearly documented with actionable guidance. No gem/package runtime code is touched, so the risk scope is appropriately narrow.

Findings

serializeForInlineScript.ts — correct implementation. JSON.stringify(value) ?? 'null' handles undefined/Symbol/function inputs, and escaping <, >, U+2028, U+2029 is the right set: < prevents </script> parse breakout, the line/paragraph separators prevent string-literal injection in JavaScript context, > is conservative-but-harmless. Round-trip test via JSON.parse(serialized) and the undefinednull case are both covered. ✓

application_controller.rbERB::Util.html_escape(nonce) before attribute interpolation, ActiveSupport::JSON.encode for JS string values, ERB::Util.html_escape for the redirect URL in the <a href. All the right escaping functions in the right places. ✓

content_security_policy.rbSecureRandom.base64(16) per-request nonce, nonce_directives = %w[script-src] (style-src deliberately excluded per issue #3862), dev carve-outs scoped to Rails.env.development?. Clean. ✓

error_scenarios_hub.html.erb — the named-window-handler pattern (window.reactOnRailsConfigOverrideChangeHandler) to prevent stacking duplicate change listeners is a good pattern for Turbo-style navigation. ✓

Comments (inline below)

Two items worth a second look posted inline: an unused require in the docs recipe code block, and a fallback path in the E2E bundle-source fetch that could silently mis-classify violations.

Comment thread docs/pro/strict-csp.md Outdated
Comment thread react_on_rails_pro/spec/dummy/e2e-tests/strict_csp.spec.ts
@claude

claude Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Code Review - PR 3934

Overall: Well-executed PR. The nonce threading through the streaming pipeline is correct, the E2E tests are properly designed, and the documentation is unusually thorough. Three minor findings below.

Finding 1 (low): script-context escaping in application_controller.rb - Lines 63 and 65 use ActiveSupport::JSON.encode which does NOT escape < > or & for script context. A value containing </script> would break out of the script tag. Inputs here are developer-controlled so real-world risk is low, but the pattern is unsafe if extended. Fix: use ERB::Util.json_escape(ActiveSupport::JSON.encode(value)) -- the Ruby-side equivalent of serializeForInlineScript on the JS side.

Finding 2 (very low): Canary test toBe(1) is slightly brittle - strict_csp.spec.ts lines 196-200 poll until violations.length equals 1. If a late-streamed violation arrived simultaneously with canary injection, the count could jump to 2 and the poll would fail as a flake. toBeGreaterThanOrEqual(1) plus a check for blockedURI === inline would be more precise.

Finding 3 (informational): serializeForInlineScript escaping is correct but could use a note - The < escape prevents </script> injection, > is defensive, and the Unicode line separators prevent line terminator injection. Neither & nor / need escaping because browsers parse script content as raw text. A brief comment would prevent future PRs from adding unnecessary escaping.

Minor observations: sourceSnippetIncludesFlightClient correctly fails closed on fetch error. The reactOnRailsConfigOverrideChangeHandler remove-before-add guard handles Turbo revisits correctly. The preload_links_header: false callout in both the layout comment and troubleshooting docs is a good call for a common footgun.

Comment thread react_on_rails_pro/spec/dummy/app/controllers/application_controller.rb Outdated
Comment thread react_on_rails_pro/spec/dummy/e2e-tests/strict_csp.spec.ts
@claude

claude Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Code Review

This is a high-quality PR that fills a real gap — streamed RSC under a strict no-unsafe-inline policy had zero automated coverage and no documentation. The approach is sound throughout.

What works well

Canary test design — the zero-violation assertion alone could be vacuous (the policy might not be enforcing at all). The canary test in strict_csp.spec.ts proves it is genuinely enforced by injecting a nonce-less script and asserting it is blocked. This is the right pattern.

Fail-closed eval toleranceisDevBuildFlightClientEval tolerates the React Flight dev-build eval violations only after positively identifying them by fetching the bundle source and checking for the known markers. Anything else fails the assertion. The fallback (bundle unreachable → treat as unexpected) is correct.

Escaping in serializeForInlineScript — Escaping < to < is sufficient to prevent </script> tag breakout (you need < to open the end-tag). Also escaping > is extra-defensive but harmless. The U+2028/U+2029 escapes are needed to prevent JavaScript string literal breakout in older environments. The ?? 'null' guard is correct: JSON.stringify(undefined) returns undefined (not the string "undefined"), so calling .replace() on it directly would throw.

Fragment caching finding — documenting the incompatibility and recommending against combining these features is the right call for now. The prerender_caching analysis (0% hit rate with per-request nonces) is a useful production gotcha that isn't obvious.

Escaping in application_controller.rb — The original code interpolated the redirect path directly into a single-quoted JS string literal (vulnerable to paths containing '). The new code uses ActiveSupport::JSON.encode + ERB::Util.json_escape, which handles all edge cases correctly.


One issue to fix

sourceSnippetIncludesFlightClient has no fetch timeout (see inline comment). The catch-on-failure fallback is correct, but a hung webpack-dev-server causes the test to wait for the browser's default fetch timeout (can be 30–60 s per violation) before the catch fires. This would make a passing run very slow if the dev server is temporarily overloaded or restarting during CI.


One docs suggestion

The troubleshooting entry for third-party <script src> tags recommends nonce: true. That works, but for CDN scripts where you know the expected content, Subresource Integrity (integrity: + crossorigin:) provides stronger posture than nonce-only — the browser verifies the script content matches the hash, so a compromised CDN cannot serve malicious content under the cover of your nonce. Worth a one-liner mention alongside the nonce: true recommendation.

Comment thread react_on_rails_pro/spec/dummy/e2e-tests/strict_csp.spec.ts
…c-streaming

* origin/main:
  Keep plan-pr-batch goal prompts under 4000 chars (#4025)
  docs(skills): file-collision check + goal-prompt size discipline for plan-pr-batch (#3914)
  Verify React 19.2 <Activity> with react_component (CSR + SSR-hydrate) + docs (#3938)
  Allow Pro RSC peer check for React 19.2 (#4026)
@justin808

Copy link
Copy Markdown
Member Author

Approved by maintainer.

…c-streaming

* origin/main:
  Expose React 19 root error callbacks (rootErrorHandlers) + hydration-mismatch debugging guide (#3933)
  Add post-merge audit scope resolver (#4029)
  Document continuous agent-run evaluation loop (#4028)
  Tools: add PR batch merge ledger (#3996)
  Add RSC FOUC acceptance coverage (#4033)

# Conflicts:
#	llms-full.txt
@justin808 justin808 merged commit 7ee628c into main Jun 15, 2026
35 of 36 checks passed
@justin808 justin808 deleted the jg/3879-strict-csp-rsc-streaming branch June 15, 2026 04:38
justin808 added a commit that referenced this pull request Jun 15, 2026
* origin/main:
  Add declarative cache_tags revalidation to Pro fragment caching (revalidateTag analog) (#3964)
  Verify and document CSP-nonce propagation for streamed RSC under a strict no-unsafe-inline policy (#3934)
  Docs: document Control Plane cost posture for demos (#3998)

# Conflicts:
#	docs/.llms-exclusions
justin808 added a commit that referenced this pull request Jun 15, 2026
…ter-slice

* origin/main:
  Add useRailsForm hook + render_model_errors concern: Inertia useForm-style Rails bridge (#3872) (#3942)
  Add declarative cache_tags revalidation to Pro fragment caching (revalidateTag analog) (#3964)
  Verify and document CSP-nonce propagation for streamed RSC under a strict no-unsafe-inline policy (#3934)
  Docs: document Control Plane cost posture for demos (#3998)
  Add built-in /health and /ready endpoints to the Pro node renderer (#3939)
  Document capacity-aware triage contracts (#4027)
justin808 added a commit that referenced this pull request Jun 15, 2026
…ce-maps

* origin/main:
  Add TanStack Router instant-navigation starter (Pro dummy) + docs guide (#3873 v1 slice) (#3953)
  Add useRailsForm hook + render_model_errors concern: Inertia useForm-style Rails bridge (#3872) (#3942)
  Add declarative cache_tags revalidation to Pro fragment caching (revalidateTag analog) (#3964)
  Verify and document CSP-nonce propagation for streamed RSC under a strict no-unsafe-inline policy (#3934)
  Docs: document Control Plane cost posture for demos (#3998)
  Add built-in /health and /ready endpoints to the Pro node renderer (#3939)
  Document capacity-aware triage contracts (#4027)
justin808 added a commit that referenced this pull request Jun 15, 2026
## Summary

Stamps the **17.0.0.rc.4** changelog header and reconciles the post-rc.3
entries.

### What changed
- **Stamped `### [17.0.0.rc.4] - 2026-06-14`** with the standard
compare-link updates (`v17.0.0.rc.3...v17.0.0.rc.4`, `unreleased →
rc.4...main`). Prior RC sections are left intact per the prerelease
convention.
- **Moved the source-mapped stack traces entry (PR 3940) into rc.4.** It
had been placed inside the already-tagged `rc.3` section, but `#3940`
merged *after* `v17.0.0.rc.3` was tagged (verified: `d9ac060a0` is not
an ancestor of the rc.3 tag).
- **Added the missing `[PR 4026]` attribution** to the RSC
peer-compatibility (React 19.2 floor) entry, which previously linked
only the issue.
- **Merged the two duplicate `#### Added` headings** in the section into
one.

### rc.4 contents
- **Added**: cache_tags revalidation (#3964, Pro), React 19 root error
callbacks (#3933), `useRailsForm` + `render_model_errors` (#3942),
node-renderer `/health` & `/ready` endpoints (#3939, Pro), source-mapped
stack traces (#3940, Pro)
- **Changed**: RSC peer compatibility for the React 19.2 floor (#4026,
Pro)
- **Fixed**: Rspack generated apps start in HMR mode (#3926)

Other merged PRs since rc.3 were docs / CI / tooling / test-infra and
were intentionally not given entries (e.g. #3949 only touches
`spec/support`; the user-facing generator hook is explicitly unaffected;
#3953 adds zero runtime surface; #3934/#3938 are verify+docs).

### Notes
- Verified clean under the repo's pinned `prettier@3.6.2`. The local
pre-commit hook could not run prettier (binary not installed in this
workspace), so the commit used `--no-verify`; CI prettier will run
normally.

After merge, run `rake release` (no args) to publish `v17.0.0.rc.4` and
auto-create the GitHub release from this section.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation-only changelog edits with no runtime or API surface
changes.
> 
> **Overview**
> Adds the **`17.0.0.rc.4` (2026-06-14)** release section to
`CHANGELOG.md` and updates the bottom compare links so
**`[unreleased]`** points at `v17.0.0.rc.4...main` and
**`[17.0.0.rc.4]`** covers `v17.0.0.rc.3...v17.0.0.rc.4`.
> 
> The edit **re-sorts notes that landed after the rc.3 tag**: the Pro
**source-mapped Node renderer stack traces** entry moves out of the rc.3
block into rc.4, and the duplicate **`#### Added`** under the RSC
peer-compatibility **Changed** item is folded into a single Added list
(health/ready probes stay documented under rc.4 Added). The React **19.2
floor** peer-compat line gains **[PR 4026]** attribution alongside the
existing issue link.
> 
> No application or library code changes—release documentation only.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
40c7a88. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Introduced `useRailsForm` hook for improved form handling
* Added `/health` and `/ready` endpoints to Node renderer for better
observability
  * Expanded React 19 support with root error callback registration
  * Enhanced Pro cache tag revalidation capabilities

* **Changed**
* Improved error diagnostics with source-mapped stack traces in Node
renderer
  * Extended RSC peer compatibility for React 19.2.7+

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@justin808

Copy link
Copy Markdown
Member Author

Manual verification: coverage/docs PR — confirmed scope, ran the new unit coverage ✅ (E2E proof not run here)

This PR (Fixes #3879) is a proof + coverage + docs change: the CSP-nonce plumbing for streamed RSC shipped back in 16.4.0 (PRs #2398/#2418); this adds a strict-CSP enforcement in the Pro dummy app, a Playwright E2E proving zero violations, a serialization helper + unit test, and docs. Its own summary states "No gem/package code changes were needed."

What I verified

  1. Scope confirmed — no shipped product code changed. The merge diff touches only react_on_rails_pro/spec/dummy/**, tests, and docs; nothing under packages/, react_on_rails/lib, react_on_rails_pro/lib, or react_on_rails_pro/app. So there is no runtime before/after to reproduce — consistent with the PR's claim that the nonce plumbing already worked and just lacked coverage.

    git show 7ee628cef --stat | grep -E 'packages/|/lib/|react_on_rails_pro/app/' | grep -v spec/dummy
    # (no output)
    
  2. New unit coverage passes (serialize-for-inline-script.test.ts):

    ✓ escapes script-closing payloads while preserving JSON round trips
    ✓ serializes undefined as null for valid inline JavaScript assignment values
    Tests: 2 passed, 2 total
    
  3. Inspected the E2E (strict_csp.spec.ts): it installs a securitypolicyviolation listener before any page script, asserts zero violations + interactive hydration under script-src 'self' 'nonce-…' (no unsafe-inline), and includes a canary that injects a nonce-less inline script and expects it blocked — so the zero-violation assertion is not vacuous. The structure is sound.

Caveat

The authoritative proof here is the Playwright E2E under the enforced strict policy, which needs a built Pro dummy app + running node renderer + browser — I did not run it in this environment. I verified the PR's scope claim (no product code change) and the new serialization unit test; the end-to-end "zero CSP violations on streamed RSC pages" assertion is verified by the committed E2E in CI (dummy-app-node-renderer-e2e-tests), not re-run by me.

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.

Verify & document CSP-nonce propagation for streamed RSC + injectRSCPayload inline scripts under a strict no-unsafe-inline policy

1 participant