Skip to content

Add CSP nonce support to RSC streaming and console replay scripts#2418

Merged
justin808 merged 9 commits intomasterfrom
codex/fix-2401-rsc-csp-nonce
Mar 10, 2026
Merged

Add CSP nonce support to RSC streaming and console replay scripts#2418
justin808 merged 9 commits intomasterfrom
codex/fix-2401-rsc-csp-nonce

Conversation

@justin808
Copy link
Copy Markdown
Member

@justin808 justin808 commented Feb 15, 2026

Summary

  • Add cspNonce to rails_context so nonce data is available in both server-side and client-side RSC flows
  • Thread nonce through Pro RSC streaming paths:
    • server-side HTML stream injection (injectRSCPayload)
    • client-side console replay script insertion (transformRSCStreamAndReplayConsoleLogs)
  • Sanitize nonce values before assigning them to script tags
  • Add regression tests for nonce injection/sanitization in RSC payload and console replay paths

Closes #2401

Test plan

  • Proposed fix (UNTESTED in this environment)
  • Intended Ruby check: bundle exec rspec react_on_rails/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
  • Intended JS checks:
    • pnpm -C packages/react-on-rails-pro test -- injectRSCPayload.test.ts
    • pnpm -C packages/react-on-rails-pro test -- transformRSCStreamAndReplayConsoleLogs.test.ts
  • Blocked locally because:
    • Ruby is 2.6.10 (project requires >= 3.0.0)
    • pnpm is not installed in this environment

Summary by CodeRabbit

  • New Features

    • Optional CSP nonce support added for server-rendered React Server Components and injected RSC scripts; server context can now include a nonce.
  • Security

    • Nonces are sanitized and unsafe attributes removed before being applied to injected or replayed scripts to improve CSP compliance.
  • Tests

    • New tests cover nonce propagation, sanitization, and replayed-console script behavior.

Note

Medium Risk
Touches streaming HTML/script injection paths and Rails context shaping; while behavior is mostly additive, mistakes could break hydration/streaming or CSP compliance.

Overview
Adds end-to-end CSP nonce support for React Server Components by exposing cspNonce in the Rails rails_context and threading it through both server and client RSC flows.

Server streaming now injects RSC payload <script> tags with a sanitized nonce (via injectRSCPayload and its caller streamServerRenderedReactComponent), and client-side RSC rendering/replayed console scripts also apply the sanitized nonce (via transformRSCStreamAndReplayConsoleLogs and getReactServerComponent.client). New unit/spec tests cover nonce propagation and sanitization to prevent attribute injection.

Written by Cursor Bugbot for commit 62b60f1. Configure here.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 15, 2026

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

Threads an optional CSP nonce from Rails through the Node RSC renderer into injected/preloaded RSC payload scripts and client-side console-replay scripts; nonce values are sanitized before being assigned to generated <script> elements. Tests and a Rails helper were added to surface and verify the nonce.

Changes

Cohort / File(s) Summary
Rails context & helper
packages/react-on-rails/src/types/index.ts, react_on_rails/lib/react_on_rails/helper.rb
Add optional cspNonce?: string to RailsContext and populate it from csp_nonce via new add_csp_nonce_to_context(result) called from rails_context.
Server renderer entrypoints
packages/react-on-rails-pro/src/getReactServerComponent.client.ts, packages/react-on-rails-pro/src/streamServerRenderedReactComponent.ts
Pass railsContext.cspNonce into RSC creation paths and to injectRSCPayload so server-side payload and stream flows receive the nonce.
RSC payload injection
packages/react-on-rails-pro/src/injectRSCPayload.ts
Extend createScriptTag, createRSCPayloadInitializationScript, createRSCPayloadChunk, and injectRSCPayload signatures to accept cspNonce?; add nonce attribute generation and sanitization when emitting inline <script> tags.
Client console-replay transformer
packages/react-on-rails-pro/src/transformRSCStreamAndReplayConsoleLogs.ts
Add optional cspNonce? parameter, use sanitizeNonce, and set sanitized nonce on dynamically created replay <script> elements while stripping unsafe attributes.
Utilities
packages/react-on-rails-pro/src/utils.ts
Add sanitizeNonce(nonce?: string) to remove characters not in the allowed set for nonce attributes.
Tests
packages/react-on-rails-pro/tests/injectRSCPayload.test.ts, packages/react-on-rails-pro/tests/transformRSCStreamAndReplayConsoleLogs.test.ts, react_on_rails/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
Add tests verifying nonce presence/absence in rails_context, nonce sanitization, and that injected/replayed scripts receive a sanitized nonce and unsafe attributes are removed.

Sequence Diagram(s)

sequenceDiagram
    participant Rails as Rails Server
    participant Node as Node Renderer
    participant Client as Browser Client

    Rails->>Rails: extract csp_nonce
    Rails->>Node: railsContext { ..., cspNonce }
    Node->>Node: createFromFetch / createFromPreloadedPayloads(..., cspNonce)
    Node->>Node: injectRSCPayload(stream, tracker, domNodeId, cspNonce)
    Node->>Client: send HTML + inline RSC payload scripts (nonce attr)
    Client->>Client: transformRSCStreamAndReplayConsoleLogs(stream, cspNonce)
    Client->>Client: sanitizeNonce(cspNonce)
    Client->>Client: create replay <script nonce="..."> and append -> executes
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐇 I carried a nonce from Rails to the stream,

tucked it in scripts like a safe little gleam.
I nibbled the danger, stripped quotes and surprise,
now replay scripts hop and run beneath clear skies. 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% 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 clearly and concisely summarizes the primary change: adding CSP nonce support to RSC streaming and console replay scripts, which is the core objective of the pull request.
Linked Issues check ✅ Passed All requirements from issue #2401 are met: cspNonce is threaded through rails_context, injectRSCPayload.ts supports nonce in script tags, transformRSCStreamAndReplayConsoleLogs.ts sets nonce on script elements, and sanitizeNonce utility prevents CSP violations.
Out of Scope Changes check ✅ Passed All changes are directly scoped to CSP nonce support: types, helper methods, utility functions, and nonce threading through RSC streaming paths with accompanying tests. No unrelated modifications detected.
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 unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/fix-2401-rsc-csp-nonce

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.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Feb 15, 2026

Greptile Summary

This PR propagates CSP (Content Security Policy) nonce values through the React on Rails Pro RSC streaming and console replay paths, ensuring that dynamically injected <script> tags include the correct nonce attribute required by strict CSP policies.

  • Adds cspNonce to the rails_context hash (Ruby) and RailsContext type (TypeScript) so the nonce is available throughout both server-side and client-side rendering flows
  • Threads the nonce through server-side HTML stream injection (injectRSCPayload) and client-side console replay script insertion (transformRSCStreamAndReplayConsoleLogs)
  • Sanitizes nonce values with a character allowlist before inserting into script tags, preventing attribute injection attacks
  • Adds regression tests for nonce injection and sanitization in both RSC payload and console replay paths
  • Note: The PR description states this is untested locally due to environment constraints (Ruby 2.6.10, no pnpm). CI verification is important before merging.
  • Minor concern: The nonce sanitization regex is duplicated across two files — extracting it to a shared utility would improve maintainability

Confidence Score: 4/5

  • This PR is safe to merge after CI passes — it adds CSP nonce support through well-understood plumbing patterns with appropriate sanitization.
  • The changes are straightforward parameter threading with proper sanitization. The nonce allowlist regex is correct for base64-encoded CSP nonces. The only concern is the duplicated sanitization logic and that the author could not run tests locally, making CI verification essential.
  • Pay attention to injectRSCPayload.ts and transformRSCStreamAndReplayConsoleLogs.ts — these contain the duplicated sanitization logic and are the security-sensitive files where nonces are applied to script tags.

Important Files Changed

Filename Overview
packages/react-on-rails-pro/src/injectRSCPayload.ts Adds cspNonce parameter threaded through createScriptTag, createRSCPayloadInitializationScript, createRSCPayloadChunk, and the main injectRSCPayload function. Includes a nonceAttribute helper with HTML-injection-safe sanitization. Nonce sanitization logic is duplicated with transformRSCStreamAndReplayConsoleLogs.ts.
packages/react-on-rails-pro/src/transformRSCStreamAndReplayConsoleLogs.ts Adds cspNonce parameter and sanitizeNonce helper; sets nonce on dynamically created script elements for console replay. Sanitization regex is duplicated from injectRSCPayload.ts.
packages/react-on-rails-pro/src/getReactServerComponent.client.ts Threads cspNonce from railsContext through createFromFetch, fetchRSC, createFromPreloadedPayloads, and getReactServerComponent. Straightforward parameter plumbing with no logic changes.
packages/react-on-rails-pro/src/streamServerRenderedReactComponent.ts Passes railsContext.cspNonce to injectRSCPayload in the onShellReady callback. Minor formatting change to multi-line the function call.
packages/react-on-rails/src/types/index.ts Adds optional cspNonce?: string to the RailsContext type. Keeps types in sync with the Ruby-side rails_context helper.
react_on_rails/lib/react_on_rails/helper.rb Adds cspNonce to the rails_context hash using the existing csp_nonce helper. Placed inside the memoized `@rails_context
react_on_rails/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb Adds two specs for cspNonce presence/absence in rails_context. Tests correctly stub csp_nonce to verify both paths.
packages/react-on-rails-pro/tests/injectRSCPayload.test.ts Adds a test verifying nonce sanitization strips dangerous characters from the nonce before injecting into script tags.
packages/react-on-rails-pro/tests/transformRSCStreamAndReplayConsoleLogs.test.ts New test file with two tests: verifies nonce is set on replayed console script elements and that malicious nonce values are sanitized.

Flowchart

flowchart TD
    A["Rails helper.rb\n(rails_context)"] -->|"cspNonce"| B["RailsContext type\n(types/index.ts)"]
    B -->|"Server-side path"| C["streamServerRenderedReactComponent"]
    C -->|"railsContext.cspNonce"| D["injectRSCPayload"]
    D -->|"nonceAttribute(cspNonce)"| E["Script tags with nonce\n(RSC payload injection)"]
    B -->|"Client-side path"| F["getReactServerComponent.client"]
    F -->|"railsContext.cspNonce"| G["transformRSCStreamAndReplayConsoleLogs"]
    G -->|"sanitizeNonce(cspNonce)"| H["Script elements with nonce\n(console replay)"]
Loading

Last reviewed commit: fe948e6

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

9 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread packages/react-on-rails-pro/src/transformRSCStreamAndReplayConsoleLogs.ts Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 15, 2026

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 62.63 KB (+0.05% 🔺)
react-on-rails/client bundled (gzip) (time) 62.63 KB (+0.05% 🔺)
react-on-rails/client bundled (brotli) 53.74 KB (+0.17% 🔺)
react-on-rails/client bundled (brotli) (time) 53.74 KB (+0.17% 🔺)
react-on-rails-pro/client bundled (gzip) 63.63 KB (+0.05% 🔺)
react-on-rails-pro/client bundled (gzip) (time) 63.63 KB (+0.05% 🔺)
react-on-rails-pro/client bundled (brotli) 54.62 KB (+0.01% 🔺)
react-on-rails-pro/client bundled (brotli) (time) 54.62 KB (+0.01% 🔺)
registerServerComponent/client bundled (gzip) 127.44 KB (+0.06% 🔺)
registerServerComponent/client bundled (gzip) (time) 127.44 KB (+0.06% 🔺)
registerServerComponent/client bundled (brotli) 61.6 KB (-0.02% 🔽)
registerServerComponent/client bundled (brotli) (time) 61.6 KB (-0.02% 🔽)
wrapServerComponentRenderer/client bundled (gzip) 122.02 KB (+0.07% 🔺)
wrapServerComponentRenderer/client bundled (gzip) (time) 122.02 KB (+0.07% 🔺)
wrapServerComponentRenderer/client bundled (brotli) 56.77 KB (+0.22% 🔺)
wrapServerComponentRenderer/client bundled (brotli) (time) 56.77 KB (+0.22% 🔺)

@justin808 justin808 added codex PRs created from codex-named branches release:16.4.0-must-have Must-have for 16.4.0: critical bug/perf/usability labels Feb 25, 2026
@justin808 justin808 added the P1 Target this sprint label Mar 5, 2026
@justin808 justin808 self-assigned this Mar 5, 2026
@justin808 justin808 requested a review from AbanoubGhadban March 8, 2026 08:45
@justin808 justin808 force-pushed the codex/fix-2401-rsc-csp-nonce branch from 62b60f1 to 0fd9d96 Compare March 8, 2026 09:09
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 8, 2026

PR Review: CSP nonce support for RSC streaming. The approach is solid overall -- threading cspNonce through Rails context into both server-side HTML stream injection and client-side console replay is the right pattern. The Ruby side is clean; csp_nonce is already well-guarded by the existing respond_to? check and Rails version fallback at helper.rb:493. See inline comments for specific issues found.

return error instanceof Error ? error.message : String(error);
};

export const sanitizeNonce = (nonce?: string) => nonce?.replace(/[^a-zA-Z0-9+/=_-]/g, '');
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.

The regex /[^a-zA-Z0-9+/=_-]/g keeps = (it is a valid Base64 padding character and is explicitly in the allow-list). However both new test files pass inputs like 'abc123" onload=alert(1)' and assert that the = is stripped, producing abc123onloadalert1. The actual output will be abc123onload=alert1 because = survives the regex, causing all three sanitization assertions to fail.

Two ways to resolve this, depending on intent:

Option A — keep = (correct for real Base64 nonces), fix the tests:
Update test expected values to abc123onload=alert1 and abc123onclick=alert1, and remove the not.toContain('onload=') assertion (the sanitized string still contains onload= as part of the nonce value, but that is harmless since the " closing the attribute has already been stripped).

Option B — strip = too, fix the regex:

Suggested change
export const sanitizeNonce = (nonce?: string) => nonce?.replace(/[^a-zA-Z0-9+/=_-]/g, '');
export const sanitizeNonce = (nonce?: string) => nonce?.replace(/[^a-zA-Z0-9+/_-]/g, '');

Note: stripping = would break nonces with Base64 padding, so Option A is generally safer for real-world nonces.

const result = injectRSCPayload(mockHTML, rscRequestTracker, domNodeId, 'abc123" onload=alert(1)');
const resultStr = await collectStreamData(result);

expect(resultStr).toContain('<script nonce="abc123onloadalert1">');
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.

This assertion will fail. sanitizeNonce allows = (it is in the regex allow-list), so the input 'abc123" onload=alert(1)' sanitizes to abc123onload=alert1, not abc123onloadalert1.

If the intent is to keep = (the right choice for Base64 nonces), this line and the next assertion need to be updated:

Suggested change
expect(resultStr).toContain('<script nonce="abc123onloadalert1">');
expect(resultStr).toContain('<script nonce="abc123onload=alert1">');

const resultStr = await collectStreamData(result);

expect(resultStr).toContain('<script nonce="abc123onloadalert1">');
expect(resultStr).not.toContain('onload=');
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.

This assertion will also fail. The sanitized nonce is abc123onload=alert1, and the resulting HTML is <script nonce="abc123onload=alert1">, which does contain the substring onload=. However, this is not a security issue — the " that would close the attribute has been stripped, so there is no attribute injection. The check should be for the real injection vector instead:

Suggested change
expect(resultStr).not.toContain('onload=');
expect(resultStr).not.toContain('onload=alert');

await collectStream(transformedStream);

const script = document.body.querySelector('script');
expect(script?.getAttribute('nonce')).toBe('abc123onclickalert1');
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.

Same regex issue — = is kept by sanitizeNonce, so 'abc123" onclick=alert(1)' produces abc123onclick=alert1, not abc123onclickalert1. This assertion will fail.

Suggested change
expect(script?.getAttribute('nonce')).toBe('abc123onclickalert1');
expect(script?.getAttribute('nonce')).toBe('abc123onclick=alert1');

Comment thread packages/react-on-rails-pro/src/utils.ts Outdated
Copy link
Copy Markdown
Collaborator

@AbanoubGhadban AbanoubGhadban left a comment

Choose a reason for hiding this comment

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

LGTM. One optional nit: the sanitization regex allows = anywhere in the string, while the CSP3 spec only permits it as trailing padding. Using /^[a-zA-Z0-9+/_-]+={0,2}$/ would be spec-exact, but it's not a security concern — just conformance.

@justin808 justin808 force-pushed the codex/fix-2401-rsc-csp-nonce branch from 0fd9d96 to 51ae4fd Compare March 9, 2026 05:09
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 9, 2026

Code Review

Overall this is a solid, well-structured addition. CSP nonce support threads cleanly through both the Ruby and TypeScript layers, the sanitization logic is sound, and the defence-in-depth approach (strip + format-validate) is the right pattern for nonces. A few items worth addressing before merge are noted below with inline comments.

Summary of findings

Correctness

  • The two-step sanitization (replace then match) is correct and consistent between RenderUtils.ts and the new sanitizeNonce utility. The regex edge cases (mid-string =, empty input, all-padding input) all behave properly.
  • add_csp_nonce_to_context safely delegates to the pre-existing csp_nonce helper which already guards against missing content_security_policy_nonce via respond_to?.

Test gaps

  • injectRSCPayload.test.ts only covers the rejection path (malicious nonce → no attribute emitted). There is no test asserting that a valid nonce is correctly written into the injected <script> tags. This is the most important coverage gap.
  • transformRSCStreamAndReplayConsoleLogs.test.ts uses getAttribute('nonce') to verify the value. JSDOM reflects IDL-set nonces back through getAttribute, but real browsers hide nonces from attribute inspection as a security measure. The assertion will pass in tests but the approach is JSDOM-specific — consider using script.nonce instead.

Performance / design

  • nonceAttribute() calls sanitizeNonce() on every chunk/script-tag creation inside the streaming path. The nonce never changes within a single stream; computing it once outside the per-chunk functions would be cleaner.
  • The exact sanitization logic is duplicated between packages/react-on-rails/src/RenderUtils.ts and packages/react-on-rails-pro/src/utils.ts. This is expected across package boundaries, but there is no cross-reference comment, creating a small future-divergence risk.

Minor

  • The test plan explicitly says "UNTESTED in this environment" — please confirm CI passes before merging, especially the Ruby specs which exercise the new add_csp_nonce_to_context path.

Comment thread packages/react-on-rails/src/RenderUtils.ts
- Add happy-path test for valid nonce in injectRSCPayload
- Use script.nonce IDL property instead of getAttribute('nonce') in test
- Memoize sanitizeNonce call once at injectRSCPayload entry point
- Add cross-package sync comment in RenderUtils.ts

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 9, 2026

test comment - please ignore (deleted)

Comment thread packages/react-on-rails-pro/src/utils.ts
Comment thread packages/react-on-rails/src/RenderUtils.ts
expect(resultStr).not.toContain('nonce=');
expect(resultStr).not.toContain('onload=');
});

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.

The test is correct but the reason why the nonce is rejected may not be obvious to future readers. After stripping the disallowed characters (", space, (, )), the input becomes abc123onload=alert1. This then fails the base64 format regex because = appears in the middle of the string rather than only as trailing padding — so the whole nonce is rejected.

A short comment would make this clearer:

Suggested change
// The malicious nonce, after stripping disallowed chars (quotes, parens, spaces),
// becomes "abc123onload=alert1". This fails the base64 format check because '='
// appears mid-string rather than as trailing padding, so the nonce is rejected entirely.
const result = injectRSCPayload(mockHTML, rscRequestTracker, domNodeId, 'abc123" onload=alert(1)');


export default function transformRSCStreamAndReplayConsoleLogs(
stream: ReadableStream<Uint8Array | string>,
cspNonce?: string,
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.

Nit: there's an extra blank line here that was added by this PR (between the JSDoc block and export default function). The blank line is unnecessary.

Suggested change
cspNonce?: string,
export default function transformRSCStreamAndReplayConsoleLogs(

@justin808 justin808 merged commit 77547ed into master Mar 10, 2026
57 checks passed
@justin808 justin808 deleted the codex/fix-2401-rsc-csp-nonce branch March 10, 2026 02:59
justin808 added a commit that referenced this pull request Mar 10, 2026
- Stamp version header for 16.4.0.rc.7
- Collapse rc.6 section into rc.7 (single prerelease section)
- Add new entries: #2418 (CSP nonce for RSC), #2421 (babel preset),
  #2581 (doctor false positives)
- Remove duplicate PR 2407 entry (appeared in both sections)
- Update version diff links

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex PRs created from codex-named branches enhancement P1 Target this sprint release:16.4.0-must-have Must-have for 16.4.0: critical bug/perf/usability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add CSP nonce to RSC streaming and console replay scripts

2 participants