Skip to content

Conversation

@justin808
Copy link
Member

@justin808 justin808 commented Nov 19, 2025

Summary

Fixes #1930

Adds Content Security Policy (CSP) nonce support for the consoleReplay script to prevent CSP violations when using script-src :self or similar policies.

Changes

TypeScript Changes

  • Added optional nonce parameter to wrapInScriptTags() function in RenderUtils.ts
  • Added optional nonce parameter to buildConsoleReplay() function
  • Added new getConsoleReplayScript() method to ReactOnRails interface and implementation
    • Returns JavaScript code without wrapping it in script tags
    • Allows Ruby to wrap the code with proper CSP nonce

Ruby Changes

  • Added wrap_console_script_with_nonce() helper method in helper.rb
    • Uses Rails' content_security_policy_nonce(:script) to get the nonce
    • Wraps console script in a <script> tag with id="consoleReplayLog" and nonce attribute
  • Updated build_react_component_result_for_server_rendered_string() to wrap console scripts with nonce
  • Updated build_react_component_result_for_server_rendered_hash() to wrap console scripts with nonce
  • Fixed bug: Changed consoleLogScript to consoleReplayScript in server_render_js() method
  • Updated JavaScript to call getConsoleReplayScript() instead of buildConsoleReplay()
  • Pro helper also updated to wrap streaming console scripts with nonce

How It Works

  1. JavaScript now returns just the console replay code (without script tags) via getConsoleReplayScript()
  2. Ruby receives this code and wraps it in a <script> tag with the CSP nonce from Rails
  3. The nonce is automatically generated by Rails when CSP is configured
  4. The script tag is rendered with the nonce attribute, allowing it to execute under CSP policies

Test Plan

  • ✅ Linting passed (ESLint, RuboCop, Prettier)
  • ✅ TypeScript builds successfully
  • 🔄 Existing RSpec tests need webpack bundle rebuild (in progress)

Breaking Changes

None. This is fully backward compatible:

  • If CSP is not configured, the helper returns an empty nonce and the script tag works as before
  • Existing buildConsoleReplay() method still works for client-side usage
  • New getConsoleReplayScript() method is additive

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Console replay now includes CSP nonce support, sanitizes nonces, and always wraps replay output in a nonce-aware script tag.
    • New helper and API methods added to obtain raw console replay code and the nonce-wrapped script.
  • Tests

    • Added tests for nonce inclusion, nonce sanitization, and console replay output/format.
  • Documentation

    • Changelog updated to note CSP nonce support for console replay.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

Warning

Rate limit exceeded

@justin808 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 16 minutes and 20 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 0740ba6 and fe863b6.

📒 Files selected for processing (2)
  • react_on_rails_pro/spec/dummy/spec/requests/renderer_console_logging_spec.rb (1 hunks)
  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (1 hunks)

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add CSP nonce support for consoleReplay script' accurately and concisely summarizes the primary objective of this changeset.
Linked Issues check ✅ Passed All objectives from #1930 are implemented: CSP nonce support for consoleReplay via Rails' content_security_policy_nonce(), backward compatibility maintained, and client/server APIs provided.
Out of Scope Changes check ✅ Passed All changes are directly related to adding CSP nonce support for consoleReplay; no unrelated modifications or scope creep detected.

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.

@claude
Copy link

claude bot commented Nov 19, 2025

Code Review: CSP Nonce Support for consoleReplay Script

Thank you for this PR! Overall, this is a well-thought-out implementation that addresses CSP violations for the console replay script. Here's my comprehensive review:

Strengths

  1. Clean API Design: The separation between buildConsoleReplay() (with script tags) and getConsoleReplayScript() (without tags) is a good design pattern that allows flexibility for different use cases.

  2. Backward Compatibility: The implementation maintains backward compatibility - existing buildConsoleReplay() calls continue to work, and CSP nonce is optional.

  3. Proper Security Approach: Using Rails' built-in content_security_policy_nonce(:script) is the correct approach, ensuring compatibility with Rails' CSP implementation.

  4. Bug Fix Included: Good catch on the consoleLogScriptconsoleReplayScript typo fix in server_render_js().

  5. Documentation: The JSDoc comments clearly explain the purpose of the new method.


🔍 Issues & Recommendations

1. Critical: Inconsistency in server_render_js method (lib/react_on_rails/helper.rb:229)

The diff shows that line 229 was changed from:

consoleReplayScript = ReactOnRails.buildConsoleReplay();

to:

consoleReplayScript = ReactOnRails.getConsoleReplayScript();

However, lines 244-246 still reference the old variable name:

html = result["html"]
console_log_script = result["consoleLogScript"]  # ❌ Wrong key name
raw("\#{html}\#{console_log_script if render_options.replay_console}")

This should be:

html = result["html"]
console_script = result["consoleReplayScript"]  # ✅ Matches the JS code
console_script_tag = wrap_console_script_with_nonce(console_script) if render_options.replay_console
raw("\#{html}\#{console_script_tag}")

Without this fix, the server_render_js method won't properly apply the CSP nonce to console replay scripts.


2. Missing Test Coverage

While the existing TypeScript tests for consoleReplay() are good, there are no tests for:

  1. The new getConsoleReplayScript() method - Should verify it returns script content without tags
  2. The wrap_console_script_with_nonce() helper - Should test:
    • Returns empty string when input is blank
    • Includes nonce attribute when CSP is configured
    • Works without nonce when CSP is not configured
    • Properly escapes script content with .html_safe
  3. Integration test - Verify the full flow produces script tags with nonce attributes

Recommendation: Add tests in:

  • packages/react-on-rails/tests/buildConsoleReplay.test.js for getConsoleReplayScript()
  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb for wrap_console_script_with_nonce()

3. Security: XSS Protection Verification

The wrap_console_script_with_nonce method uses:

content_tag(:script, console_script_code.html_safe, script_options)

Since the console script code comes from getConsoleReplayScript() which calls scriptSanitizedVal() on all arguments, this should be safe. However, it would be good to:

  1. Add a comment explaining why .html_safe is safe here (because the content is already sanitized via scriptSanitizedVal)
  2. Verify that all paths to this code properly sanitize input

4. Minor: Nonce Parameter Not Used in TypeScript

The buildConsoleReplay() function now accepts an optional nonce parameter:

export default function buildConsoleReplay(
  customConsoleHistory: (typeof console)['history'] | undefined = undefined,
  numberOfMessagesToSkip: number = 0,
  nonce?: string,
): string

However, this parameter is only used in the TypeScript version, not in the Ruby-wrapped version. This is fine for backward compatibility, but the documentation could clarify:

  • When to use buildConsoleReplay() with nonce (client-side scenarios)
  • When to use getConsoleReplayScript() (server-side rendering with Rails CSP)

5. Code Style: Blank Line Before Method

Per RuboCop conventions, consider adding a blank line before the new wrap_console_script_with_nonce method definition (line 439 in helper.rb) to maintain consistent spacing.


🧪 Testing Recommendations

  1. Manual Testing Checklist:

    • ✅ Test with CSP enabled: script-src 'self' 'nonce-{random}'
    • ✅ Test with CSP disabled
    • ✅ Test with replay_console: true and replay_console: false
    • ✅ Test server-rendered components
    • ✅ Test server_render_js method
  2. Automated Tests (add these):

    # In spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
    describe "#wrap_console_script_with_nonce" do
      let(:helper) { PlainReactOnRailsHelper.new }
      let(:script_code) { "console.log('test');" }
    
      it "returns empty string for blank input" do
        expect(helper.send(:wrap_console_script_with_nonce, "")).to eq("")
        expect(helper.send(:wrap_console_script_with_nonce, nil)).to eq("")
      end
    
      context "with CSP nonce available" do
        it "includes nonce in script tag" do
          allow(helper).to receive(:content_security_policy_nonce).with(:script).and_return("abc123")
          result = helper.send(:wrap_console_script_with_nonce, script_code)
          expect(result).to include('nonce="abc123"')
          expect(result).to include('id="consoleReplayLog"')
        end
      end
    
      context "without CSP nonce" do
        it "creates script tag without nonce" do
          result = helper.send(:wrap_console_script_with_nonce, script_code)
          expect(result).not_to include('nonce=')
          expect(result).to include('id="consoleReplayLog"')
        end
      end
    end

📋 Before Merging Checklist

  • Fix the server_render_js inconsistency (critical)
  • Add tests for wrap_console_script_with_nonce
  • Add tests for getConsoleReplayScript()
  • Run full test suite: rake
  • Run linting: bundle exec rubocop
  • Test manually with CSP enabled/disabled
  • Update CHANGELOG.md if not already done

🎯 Overall Assessment

This is a solid PR that addresses a real pain point with CSP configurations. The approach is sound and maintains backward compatibility well. The main blocker is the inconsistency in server_render_js that needs to be fixed, and adding test coverage would make this production-ready.

Rating: Good implementation with one critical fix needed + test coverage recommended.

Great work on this feature! 🚀

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5b13e0 and 6cd600f.

📒 Files selected for processing (6)
  • lib/react_on_rails/helper.rb (5 hunks)
  • packages/react-on-rails/src/RenderUtils.ts (1 hunks)
  • packages/react-on-rails/src/base/client.ts (2 hunks)
  • packages/react-on-rails/src/buildConsoleReplay.ts (2 hunks)
  • packages/react-on-rails/src/types/index.ts (1 hunks)
  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb (1 hunks)
🧰 Additional context used
🧠 Learnings (12)
📓 Common learnings
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1696
File: node_package/src/RSCPayloadContainer.ts:0-0
Timestamp: 2025-04-09T12:56:10.756Z
Learning: In the react_on_rails codebase, RSC payloads are already stringified using `JSON.stringify()` before being processed by the `escapeScript` function, which handles escaping of special characters. The function only needs to handle specific HTML markers like comments and closing script tags.
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1745
File: node_package/src/RSCRequestTracker.ts:8-14
Timestamp: 2025-07-08T05:57:29.630Z
Learning: The global `generateRSCPayload` function in React on Rails Pro (RORP) is provided by the framework during rendering requests, not implemented in application code. The `declare global` statements are used to document the expected interface that RORP will inject at runtime.
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation is handled in deeper level calls of the React on Rails Pro codebase, so it doesn't need to be validated again in the `rsc_payload_react_component` helper method.
📚 Learning: 2025-04-09T12:56:10.756Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1696
File: node_package/src/RSCPayloadContainer.ts:0-0
Timestamp: 2025-04-09T12:56:10.756Z
Learning: In the react_on_rails codebase, RSC payloads are already stringified using `JSON.stringify()` before being processed by the `escapeScript` function, which handles escaping of special characters. The function only needs to handle specific HTML markers like comments and closing script tags.

Applied to files:

  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
  • lib/react_on_rails/helper.rb
📚 Learning: 2025-07-08T05:57:29.630Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1745
File: node_package/src/RSCRequestTracker.ts:8-14
Timestamp: 2025-07-08T05:57:29.630Z
Learning: The global `generateRSCPayload` function in React on Rails Pro (RORP) is provided by the framework during rendering requests, not implemented in application code. The `declare global` statements are used to document the expected interface that RORP will inject at runtime.

Applied to files:

  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
  • packages/react-on-rails/src/types/index.ts
  • packages/react-on-rails/src/buildConsoleReplay.ts
  • lib/react_on_rails/helper.rb
📚 Learning: 2025-04-26T21:55:55.874Z
Learnt from: alexeyr-ci2
Repo: shakacode/react_on_rails PR: 1732
File: spec/dummy/client/app-react16/startup/ReduxSharedStoreApp.client.jsx:40-44
Timestamp: 2025-04-26T21:55:55.874Z
Learning: In the react_on_rails project, files under `app-react16` directories are copied/moved to corresponding `/app` directories during the conversion process (removing the `-react16` suffix), which affects their relative import paths at runtime.

Applied to files:

  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
  • packages/react-on-rails/src/base/client.ts
📚 Learning: 2025-09-16T08:01:11.146Z
Learnt from: justin808
Repo: shakacode/react_on_rails PR: 1770
File: lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx:2-2
Timestamp: 2025-09-16T08:01:11.146Z
Learning: React on Rails uses webpack CSS Modules configuration with namedExports: true, which requires the import syntax `import * as style from './file.module.css'` rather than the default export pattern. This configuration enables better tree shaking and bundle size optimization for CSS modules.

Applied to files:

  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
  • packages/react-on-rails/src/base/client.ts
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation is handled in deeper level calls of the React on Rails Pro codebase, so it doesn't need to be validated again in the `rsc_payload_react_component` helper method.

Applied to files:

  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
  • lib/react_on_rails/helper.rb
📚 Learning: 2025-02-12T16:38:06.537Z
Learnt from: Romex91
Repo: shakacode/react_on_rails PR: 1697
File: package-scripts.yml:28-28
Timestamp: 2025-02-12T16:38:06.537Z
Learning: The file `node_package/lib/ReactOnRails.full.js` is autogenerated during the build process and should not be present in the repository.

Applied to files:

  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
  • packages/react-on-rails/src/types/index.ts
  • packages/react-on-rails/src/base/client.ts
📚 Learning: 2025-02-13T16:50:47.848Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/clientStartup.ts:18-21
Timestamp: 2025-02-13T16:50:47.848Z
Learning: In the react_on_rails module, the `reactOnRailsPageUnloaded` function in clientStartup.ts is intentionally kept private as it's only used internally as a callback for `onPageUnloaded`.

Applied to files:

  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.

Applied to files:

  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
📚 Learning: 2025-09-15T21:24:48.207Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1781
File: node_package/src/ClientSideRenderer.ts:82-95
Timestamp: 2025-09-15T21:24:48.207Z
Learning: In React on Rails, the force_load feature includes both explicit `data-force-load="true"` usage and the ability to hydrate components during the page loading state (`document.readyState === 'loading'`). Both capabilities require a Pro license, so the condition `!railsContext.rorPro && (isComponentForceLoaded || document.readyState === 'loading')` correctly gates both scenarios.

Applied to files:

  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
📚 Learning: 2024-12-12T13:07:09.929Z
Learnt from: alexeyr-ci
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/ReactOnRailsRSC.ts:87-87
Timestamp: 2024-12-12T13:07:09.929Z
Learning: When handling errors in 'node_package/src/ReactOnRailsRSC.ts', include the error stack in error messages in development and test environments to aid debugging.

Applied to files:

  • packages/react-on-rails/src/types/index.ts
  • packages/react-on-rails/src/base/client.ts
📚 Learning: 2025-02-13T19:09:15.991Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/RSCWebpackLoader.ts:0-0
Timestamp: 2025-02-13T19:09:15.991Z
Learning: In React Server Components webpack loader, using `new Function('return import("react-server-dom-webpack/node-loader")')()` is necessary as a workaround to bypass TypeScript compilation issues with direct dynamic imports.

Applied to files:

  • packages/react-on-rails/src/base/client.ts
🧬 Code graph analysis (4)
react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb (2)
lib/react_on_rails/react_component/render_options.rb (1)
  • replay_console (82-84)
lib/react_on_rails/helper.rb (1)
  • wrap_console_script_with_nonce (440-451)
packages/react-on-rails/src/buildConsoleReplay.ts (1)
packages/react-on-rails/src/RenderUtils.ts (1)
  • wrapInScriptTags (2-13)
packages/react-on-rails/src/base/client.ts (1)
packages/react-on-rails/src/buildConsoleReplay.ts (1)
  • consoleReplay (18-55)
lib/react_on_rails/helper.rb (1)
lib/react_on_rails/react_component/render_options.rb (1)
  • replay_console (82-84)
🔇 Additional comments (7)
packages/react-on-rails/src/base/client.ts (1)

178-180: LGTM!

The new getConsoleReplayScript() method correctly delegates to consoleReplay() and provides a clean API for retrieving unwrapped console replay JavaScript. This allows callers (like the Ruby helper) to wrap the script with their own nonce-aware tags.

react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb (1)

350-351: LGTM!

The console script is properly extracted and wrapped with nonce support when replay is enabled. This implementation is consistent with the nonce-wrapping pattern used throughout the codebase.

packages/react-on-rails/src/types/index.ts (1)

440-447: LGTM!

The type definition and documentation for getConsoleReplayScript() are clear and accurate. The JSDoc effectively communicates the distinction between this method and buildConsoleReplay(), and explains the CSP nonce use case.

packages/react-on-rails/src/buildConsoleReplay.ts (1)

60-66: LGTM with dependency on RenderUtils fix.

The nonce parameter is correctly threaded through to wrapInScriptTags. The optional parameter maintains backward compatibility. However, ensure the HTML escaping issue flagged in RenderUtils.ts (line 7) is addressed, as this code depends on safe nonce handling in that function.

lib/react_on_rails/helper.rb (3)

229-229: LGTM!

The change from buildConsoleReplay() to getConsoleReplayScript() is correct. This retrieves the unwrapped console replay JavaScript, which is then wrapped by the Ruby helper with CSP nonce support on lines 245-247.


398-398: LGTM!

Both usages of wrap_console_script_with_nonce correctly use the ternary pattern to ensure result_console_script is always a string (either the wrapped script or an empty string). This is the correct approach and is consistent across both rendering paths.

Also applies to: 423-423


440-451: LGTM!

The wrap_console_script_with_nonce helper is well-implemented:

  • Properly guards against blank input
  • Uses Rails' content_security_policy_nonce(:script) to retrieve the CSP nonce
  • Leverages content_tag which will properly escape the nonce attribute value
  • Returns an empty string when no console script is provided

The use of html_safe on line 450 is appropriate here since console_script_code contains JavaScript code generated by the React on Rails bundle, not user input.

@claude
Copy link

claude bot commented Nov 19, 2025

Code Review: CSP Nonce Support for consoleReplay Script

Overall Assessment

This is a well-implemented feature that adds Content Security Policy (CSP) nonce support for the console replay script. The changes are backward compatible and follow good security practices.


Strengths

1. Security Design

  • Properly integrates with Rails content_security_policy_nonce helper
  • The nonce is generated by Rails automatically when CSP is configured
  • Maintains backward compatibility - works without CSP configuration

2. Architecture

  • Clean separation of concerns: JavaScript generates code, Ruby wraps it with security attributes
  • New getConsoleReplayScript() method is additive, doesn't break existing buildConsoleReplay()
  • The approach follows Rails conventions for CSP nonce handling

3. Code Quality

  • Consistent implementation across both open-source and Pro packages
  • Proper use of html_safe for trusted content
  • Good defensive programming with blank? checks

Issues and Recommendations

1. Missing Test Coverage - HIGH PRIORITY

The PR lacks test coverage for the new CSP nonce functionality. Tests should be added to verify the nonce is properly included in the script tag and that the code gracefully handles cases where CSP is not configured.

2. Missing Changelog Entry - REQUIRED

Per CLAUDE.md guidelines, this user-visible feature needs a changelog entry in /CHANGELOG.md

3. Potential XSS Risk in wrapInScriptTags - MEDIUM PRIORITY

Location: packages/react-on-rails/src/RenderUtils.ts:9

The nonce parameter is directly interpolated without sanitization. While Rails content_security_policy_nonce generates safe nonces, defense-in-depth is good practice since this is a reusable utility function. Consider sanitizing the nonce parameter.

4. Error Handling in Ruby - LOW PRIORITY

Location: lib/react_on_rails/helper.rb:444

Consider adding error handling for the content_security_policy_nonce call in case it raises an error.

5. Bug Fix Documentation

The fix changing consoleLogScript to consoleReplayScript is a legitimate bug fix that should be highlighted in the changelog as well.


Pre-Merge Checklist

Based on CLAUDE.md requirements:

  • CRITICAL: Run bundle exec rubocop and fix ALL violations
  • CRITICAL: Ensure all files end with newline character
  • REQUIRED: Add changelog entry to /CHANGELOG.md
  • REQUIRED: Add test coverage for new functionality
  • RECOMMENDED: Sanitize nonce parameter in wrapInScriptTags
  • RECOMMENDED: Add error handling in wrap_console_script_with_nonce

Summary

This is a solid implementation of an important security feature. The architecture is clean and the approach is correct. However, before merging:

  1. Must have: Test coverage for the CSP nonce functionality
  2. Must have: Changelog entry
  3. Must have: RuboCop compliance verification
  4. Should have: Input sanitization in wrapInScriptTags
  5. Should have: Error handling in Ruby helper

The security approach is sound, but defense-in-depth improvements would make it excellent.

Great work on addressing issue 1930!

@claude
Copy link

claude bot commented Nov 19, 2025

Code Review - PR #2059: Add CSP nonce support for consoleReplay script

I've reviewed this PR thoroughly. Overall, this is a well-implemented feature that addresses a real security need. Here's my detailed feedback:


Strengths

1. Excellent Architecture

  • Clean separation of concerns: TypeScript returns raw JS code, Ruby handles CSP nonce wrapping
  • The new getConsoleReplayScript() method is a great addition that doesn't break existing buildConsoleReplay() usage
  • Backward compatibility is maintained throughout

2. Security Improvements

  • Proper CSP nonce integration using Rails' built-in content_security_policy_nonce(:script)
  • The nonce is only added when available, maintaining compatibility with non-CSP configurations
  • Script content is properly sanitized via existing scriptSanitizedVal() function

3. Code Quality

  • Consistent implementation across all rendering paths (string, hash, streaming in Pro)
  • Good use of Rails helpers like content_tag() for proper HTML generation
  • html_safe is correctly used on helper.rb:450 after wrapping content in a Rails-generated tag

4. Bug Fix Included

  • Fixed the consoleLogScript to consoleReplayScript naming inconsistency on helper.rb:245

Potential Issues & Questions

1. Missing CHANGELOG Entry (Required)

According to CLAUDE.md, user-visible changes (especially new features) should be documented in CHANGELOG.md. This CSP nonce support is a user-visible feature.

2. XSS Concerns in wrapInScriptTags()

In RenderUtils.ts:7, the nonce is directly interpolated into HTML without escaping. While Rails' content_security_policy_nonce() should always return a safe base64 string, defensive programming would suggest HTML-escaping the nonce. However, the Ruby path uses Rails' safe content_tag() helper, which is secure.

Question: Is the nonce parameter in wrapInScriptTags() and buildConsoleReplay() actually used anywhere in the codebase for client-side usage? If not, consider documenting that it should only receive trusted values.

3. Missing Test Coverage (Required)

I don't see tests that verify:

  • The nonce attribute is present in the script tag when CSP is configured
  • The nonce attribute is absent when CSP is not configured
  • Integration with Rails' CSP helpers

Consider adding tests to spec/dummy/spec/requests/console_logging_spec.rb


Summary

Aspect Rating Notes
Architecture Excellent Clean separation, backward compatible
Security Good Proper CSP integration, minor XSS consideration
Code Quality Excellent Consistent, well-structured
Test Coverage Needs Work Missing CSP-specific tests
Documentation Needs Work CHANGELOG entry needed

Recommendations

Required Before Merge:

  1. Add CHANGELOG entry (per CLAUDE.md guidelines)
  2. Add test coverage for CSP nonce functionality

Nice to Have:
3. Consider adding HTML escaping for the nonce in RenderUtils.ts for defense-in-depth
4. Add a comment explaining that the nonce parameter should only receive trusted values


This is a solid implementation that solves a real problem (CSP violations) in a clean, backward-compatible way. With the addition of tests and CHANGELOG entry, this will be ready to merge!

@claude
Copy link

claude bot commented Nov 19, 2025

Code Review: CSP Nonce Support for consoleReplay Script

I've reviewed PR 2059 which adds Content Security Policy (CSP) nonce support for the console replay script. Overall, this is a well-implemented feature that addresses a real security concern.


Strengths

1. Clean Architecture

  • The separation of concerns is excellent: JavaScript generates the code, Ruby wraps it with Rails-specific CSP nonce
  • The new getConsoleReplayScript() method follows good API design
  • Backward compatibility maintained by keeping buildConsoleReplay() functional

2. Good Security Practices

  • Uses Rails built-in content_security_policy_nonce(:script) method
  • Graceful degradation when CSP is not configured
  • Content is already sanitized via scriptSanitizedVal()
  • Checks respond_to? before calling the Rails 5.2+ method

3. Strong Test Coverage

  • JavaScript tests cover all three scenarios: with nonce, without messages, and consoleReplay() alone

4. Bug Fix Included

  • Fixed typo: consoleLogScript → consoleReplayScript in server_render_js() (line 245)

Potential Issues and Suggestions

1. XSS Risk: Nonce Value Not Sanitized (SECURITY)

Location: RenderUtils.ts:7

The nonce value is interpolated directly into an HTML attribute without sanitization. While Rails content_security_policy_nonce() returns a safe base64 string, it's good practice to validate/escape the nonce value for defense in depth.

Theoretical attack: If nonce could be controlled to contain abc123 onload=alert(1), it would result in attribute injection.

Recommendation: Sanitize nonce to only allow alphanumeric and base64 characters.

Note: The Ruby side uses content_tag(:script, ...) which automatically escapes attributes, so that path is secure.

2. Missing Ruby Tests for New Method

The new wrap_console_script_with_nonce() helper method has no dedicated Ruby tests.

Recommendation: Add RSpec tests in spec/dummy/spec/helpers/react_on_rails_helper_spec.rb to verify:

  • Returns empty string when input is blank
  • Generates correct script tag with nonce when CSP is enabled
  • Generates correct script tag without nonce when CSP is disabled

3. CHANGELOG Not Updated

No entry in CHANGELOG.md for this user-visible feature. Per CLAUDE.md guidelines, this should be added.

4. Documentation Enhancement

Location: helper.rb:441

The comment could be more specific about what threats scriptSanitizedVal() mitigates.

5. Pro Package Consistency

Good catch updating react_on_rails_pro_helper.rb as well!


Performance and Security

Performance: Negligible impact - lightweight operations with no breaking changes

Security Assessment:

  • Strengths: Addresses real CSP issue, uses Rails built-in nonce generation
  • Concerns: Medium priority nonce interpolation, low priority missing test coverage

Recommendations Summary

  1. MUST FIX: Add nonce sanitization in wrapInScriptTags() for defense in depth
  2. SHOULD ADD: Ruby tests for wrap_console_script_with_nonce()
  3. SHOULD ADD: CHANGELOG entry
  4. NICE TO HAVE: Enhanced code comment

Final Verdict

This PR is nearly ready to merge after addressing the nonce sanitization concern. The implementation is solid, well-thought-out, and maintains backward compatibility.

Estimated effort: ~25 minutes total (5 min sanitization, 15-20 min tests, 2 min changelog)

Great work on this feature! The architecture decisions are particularly well done.

@claude
Copy link

claude bot commented Nov 19, 2025

Great work on addressing issue #1930! This is a well-thought-out implementation that adds proper CSP nonce support. Here is my detailed review:

Strengths

  1. Excellent backward compatibility approach - graceful fallback for Rails 5.2-6.0 vs 6.1+ API differences using try-catch
  2. Strong security approach - properly documented that html_safe is secure due to pre-sanitization via scriptSanitizedVal()
  3. Good test coverage - TypeScript tests verify nonce parameter handling
  4. Clean architecture - new getConsoleReplayScript() method provides flexibility

Potential Issues and Suggestions

1. Security: Potential XSS via nonce attribute (Medium Priority)

In RenderUtils.ts, the nonce is directly interpolated into the script tag. If the nonce value contains double quotes or other HTML special characters, it could break out of the attribute context. Recommend adding HTML attribute escaping or validating that nonces match the expected base64 format.

2. Missing RSpec tests for Ruby helper (High Priority)

The new wrap_console_script_with_nonce() method has no Ruby test coverage. Consider adding comprehensive tests to spec/dummy/spec/helpers/react_on_rails_helper_spec.rb covering:

  • When CSP nonce is available
  • When CSP is not configured
  • Rails 5.2-6.0 vs 6.1+ compatibility
  • Blank/nil input handling

3. Edge case: nil vs empty string handling (Low Priority)

At call sites (lines 246, 398, 423), the conditional check could result in nil instead of empty string. Consider using ternary operator for consistent empty string behavior.

4. Documentation: User-facing changes (Medium Priority)

Since this affects how CSP is configured, consider adding notes to README about CSP nonce support and example Rails CSP configuration.

Test Plan Suggestions

Beyond existing tests, consider adding:

  1. Integration test: Verify CSP headers work with rendered console replay scripts
  2. Cross-version test: Test Rails 5.2, 6.0, 6.1, and 7.x compatibility explicitly
  3. E2E test: Playwright test that verifies console replay works under CSP restrictions

Summary

This is a solid PR that solves a real problem. Main concerns:

  1. Critical: Add HTML escaping for nonce attribute in TypeScript
  2. Important: Add Ruby test coverage for the new helper method
  3. Nice-to-have: Improve edge case handling and documentation

Once these issues are addressed, this will be ready to merge. Excellent work on the backward compatibility and clean separation between JS and Ruby concerns!

@claude
Copy link

claude bot commented Nov 19, 2025

Code Review: Add CSP nonce support for consoleReplay script

Summary

This PR adds Content Security Policy (CSP) nonce support for the console replay script, addressing issue #1930. The implementation is well-structured and maintains backward compatibility while adding important security features.


✅ Strengths

  1. Excellent Separation of Concerns

    • Clean separation between JavaScript (generating code) and Ruby (wrapping with nonce)
    • The new getConsoleReplayScript() method provides flexibility without breaking existing APIs
    • lib/react_on_rails/helper.rb:226 - Correctly uses the new method
  2. Backward Compatibility

    • Maintains existing buildConsoleReplay() method for client-side usage
    • Works seamlessly when CSP is not configured (nonce is empty)
    • Optional nonce parameter does not break existing calls
  3. Rails Version Compatibility

    • lib/react_on_rails/helper.rb:448-456 - Excellent handling of Rails 5.2-6.0 vs 6.1+ API differences
    • Graceful fallback with ArgumentError rescue for older Rails versions
  4. Test Coverage

    • Good TypeScript test coverage in packages/react-on-rails/tests/buildConsoleReplay.test.js:78-108
    • Tests cover nonce attribute, empty console history, and script tag-less output
  5. Security Practices

    • Uses Rails built-in content_security_policy_nonce helper
    • Leverages existing scriptSanitizedVal() sanitization from JavaScript layer
    • Comment in lib/react_on_rails/helper.rb:444 correctly documents security assumption

🔍 Issues & Recommendations

1. CRITICAL: Insufficient Test Coverage for Ruby Implementation

The new wrap_console_script_with_nonce() method in helper.rb:443-463 lacks Ruby specs. This is a security-sensitive method handling nonce attributes.

Missing test scenarios:

  • Nonce attribute is correctly added when CSP is configured
  • Empty/nil console script returns empty string
  • Rails 5.2-6.0 compatibility (no-argument fallback)
  • Rails 6.1+ compatibility (with :script argument)
  • Script tag structure with correct ID attribute
  • html_safe handling

Recommendation: Add comprehensive Ruby specs for this method to spec/dummy/spec/helpers/react_on_rails_helper_spec.rb

2. Documentation: Update CHANGELOG.md

According to CLAUDE.md, user-visible changes require CHANGELOG updates. This is a feature (CSP nonce support) that should be documented.

Recommendation: Add entry to /CHANGELOG.md under Unreleased or next version describing the new CSP nonce support feature and referencing PR 2059 and issue #1930.


🔒 Security Assessment

Overall: ✅ SECURE with proper usage

  1. XSS Prevention: Uses scriptSanitizedVal() to prevent script tag breakout
  2. CSP Integration: Correctly uses Rails built-in CSP nonce generation
  3. Sanitization Flow: JavaScript → sanitize → Ruby → wrap → render (proper chain)
  4. html_safe Usage: Justified because content is pre-sanitized

No security vulnerabilities detected, but test coverage is critical to maintain this security posture.


📊 Performance Considerations

Impact: Negligible

  • Additional method call overhead is minimal
  • CSP nonce lookup is cached by Rails
  • No additional database queries or I/O
  • String concatenation is efficient for small console scripts

🎯 Overall Assessment

Rating: 8.5/10 - Excellent implementation with room for improvement in test coverage

Blocking Issues:

  • ❌ Missing Ruby specs for wrap_console_script_with_nonce() (critical for security-sensitive code)
  • ❌ Missing CHANGELOG entry (required per CLAUDE.md)

Recommendations:

  • ⚠️ Add comprehensive Ruby specs
  • ⚠️ Add integration test for CSP nonce in rendered output
  • ⚠️ Update CHANGELOG.md

Once the Ruby specs and CHANGELOG are added, this will be production-ready.


📝 Code Style & Conventions

✅ Compliant with CLAUDE.md guidelines:

  • Proper use of RuboCop (assuming it passes - PR description says ✅)
  • TypeScript tests follow existing patterns
  • Method naming follows Ruby conventions (wrap_console_script_with_nonce)
  • Documentation comments are clear and concise

🚀 Next Steps

  1. High Priority:

    • Add Ruby specs for wrap_console_script_with_nonce (blocks merge)
    • Update CHANGELOG.md (blocks merge per guidelines)
  2. Medium Priority:

    • Add integration test with actual CSP configuration
    • Enhance security documentation comment
  3. Before Merge:

    • Run bundle exec rubocop to ensure zero offenses (as per CLAUDE.md)
    • Verify all new tests pass
    • Ensure trailing newlines in all modified files

Great work on this feature! The implementation is solid and the approach is well-thought-out. With the additional test coverage, this will be a valuable security enhancement. 🎉

@claude
Copy link

claude bot commented Nov 19, 2025

Code Review: CSP Nonce Support for Console Replay

Great work on adding CSP nonce support! This is a well-implemented security enhancement. Here's my detailed review:


✅ Strengths

  1. Defense-in-Depth Security: The implementation has multiple layers of sanitization:

    • JavaScript side: scriptSanitizedVal() prevents </script> tag injection
    • Nonce sanitization: Regex [^a-zA-Z0-9+/=_-] prevents attribute injection attacks
    • Rails side: Uses content_tag helper which auto-escapes attributes
  2. Excellent Rails Compatibility: The wrap_console_script_with_nonce method handles Rails 5.2-7.x gracefully with proper fallback:

    begin
      content_security_policy_nonce(:script)
    rescue ArgumentError
      content_security_policy_nonce  # Rails 5.2-6.0 fallback
    end
  3. Backward Compatibility: Zero breaking changes - the buildConsoleReplay() method still works, and getConsoleReplayScript() is additive.

  4. Good Test Coverage: JavaScript tests cover:

    • Nonce attribute inclusion
    • Empty console history edge case
    • Script tag separation (consoleReplay() vs buildConsoleReplay())
    • XSS prevention via nonce sanitization
  5. Clean Architecture: Proper separation of concerns - JavaScript returns raw code, Ruby handles HTML generation with framework-specific features (CSP nonce).


🔍 Issues & Recommendations

1. Missing Ruby Test Coverage ⚠️ HIGH PRIORITY

The new wrap_console_script_with_nonce method has no RSpec tests. This is critical for:

  • Verifying Rails 5.2-7.x compatibility
  • Testing edge cases (blank input, missing CSP config)
  • Preventing regressions

Recommendation: Add tests to spec/dummy/spec/helpers/react_on_rails_helper_spec.rb:

describe '#wrap_console_script_with_nonce' do
  let(:console_script) { 'console.log("test");' }
  
  context 'when CSP is configured' do
    before do
      allow(helper).to receive(:content_security_policy_nonce).with(:script).and_return('abc123')
    end
    
    it 'includes nonce attribute' do
      result = helper.wrap_console_script_with_nonce(console_script)
      expect(result).to include('nonce="abc123"')
      expect(result).to include('id="consoleReplayLog"')
    end
  end
  
  context 'when CSP is not configured' do
    before do
      allow(helper).to receive(:respond_to?).with(:content_security_policy_nonce).and_return(false)
    end
    
    it 'omits nonce attribute' do
      result = helper.wrap_console_script_with_nonce(console_script)
      expect(result).not_to include('nonce=')
      expect(result).to include('<script id="consoleReplayLog">')
    end
  end
  
  context 'with blank input' do
    it 'returns empty string' do
      expect(helper.wrap_console_script_with_nonce('')).to eq('')
      expect(helper.wrap_console_script_with_nonce(nil)).to eq('')
    end
  end
end

2. Nonce Sanitization Could Be Stricter ⚠️ MEDIUM PRIORITY

Current regex: [^a-zA-Z0-9+/=_-]

Issue: The = character at the end of base64 strings could theoretically be part of XSS if other vulnerabilities exist (though unlikely with content_tag).

Recommendation: Consider restricting to base64 characters only - though current implementation is acceptable since content_tag provides attribute escaping. The test confirms malicious payloads are neutralized.

3. TypeScript Signature Could Be More Explicit 💡 LOW PRIORITY

export function wrapInScriptTags(scriptId: string, scriptBody: string, nonce?: string): string

Suggestion: Document the nonce format in JSDoc:

/**
 * @param nonce - CSP nonce value (should be base64, but will be sanitized)
 */
export function wrapInScriptTags(scriptId: string, scriptBody: string, nonce?: string): string

4. Potential Edge Case: Nonce with Only Invalid Characters 💡 LOW PRIORITY

If someone passes nonce='<>"', the sanitized result would be an empty string, resulting in nonce="".

Current behavior: Empty nonce attribute is rendered (<script nonce="">)

Suggestion: Skip nonce attribute if sanitization results in empty string:

const sanitizedNonce = nonce?.replace(/[^a-zA-Z0-9+/=_-]/g, '');
const nonceAttr = (sanitizedNonce && sanitizedNonce.length > 0) ? ` nonce="${sanitizedNonce}"` : '';

🔒 Security Assessment

Overall: SECURE

The multi-layered approach provides strong protection:

  1. ✅ JavaScript content is sanitized via scriptSanitizedVal()
  2. ✅ Nonce is sanitized to prevent attribute injection
  3. ✅ Rails content_tag escapes attribute values
  4. ✅ Tests verify XSS prevention

No security vulnerabilities identified.


📊 Performance Considerations

  • ✅ Minimal overhead: One regex replacement and one Rails helper call
  • ✅ No N+1 queries or blocking operations
  • ✅ Gracefully handles missing CSP configuration (no nonce = no extra work)

📝 Code Quality

  • ✅ Clear variable names (sanitizedNonce, nonceAttr, console_script_tag)
  • ✅ Good inline comments explaining security decisions
  • ✅ Follows project conventions (RuboCop compliant)
  • ✅ Changelog entry follows project format

🎯 Action Items

Before merging:

  1. Add RSpec tests for wrap_console_script_with_nonce (critical)
  2. Consider stricter nonce sanitization (optional)
  3. Consider handling empty sanitized nonce case (optional)

After merging:
4. Consider adding integration test with actual CSP headers


📋 Summary

This PR successfully adds CSP nonce support with excellent backward compatibility and security. The main gap is missing Ruby test coverage for the new helper method. Once tests are added, this is ready to merge!

Estimated effort to address: ~30 minutes for comprehensive RSpec tests


Great work overall! The TypeScript implementation and tests are particularly well-done. 🎉

@claude
Copy link

claude bot commented Nov 19, 2025

Code Review - PR 2059: Add CSP nonce support for consoleReplay script

Overall, this is a well-implemented PR that adds important security functionality. The implementation is thorough, backward-compatible, and includes good defense-in-depth practices.

Strengths

  1. Security Best Practices

    • Defense-in-depth with nonce sanitization in RenderUtils.ts:8
    • Proper CSP nonce integration using Rails built-in content_security_policy_nonce
    • Good XSS prevention test coverage
  2. Backward Compatibility

    • Falls back gracefully when CSP is not configured
    • Maintains existing buildConsoleReplay API
    • Cross-Rails version compatibility (5.2-7.x) with proper error handling
  3. Code Quality

    • Clean separation: JS returns code, Ruby wraps with nonce
    • Consistent application across all rendering paths
    • Good test coverage for TypeScript changes

Issues & Recommendations

1. Missing RSpec Test Coverage (HIGH PRIORITY)

The PR lacks Ruby-side tests for the new wrap_console_script_with_nonce method. No tests cover:

  • CSP nonce attribute in generated script tags
  • Rails version compatibility fallback behavior
  • Empty/nil console script handling

Recommendation: Add tests to spec/dummy/spec/helpers/react_on_rails_helper_spec.rb

2. RBS Type Signature Missing (MEDIUM)

The new wrap_console_script_with_nonce method needs a type signature in sig/react_on_rails/helper.rbs per CLAUDE.md requirements.

3. Nonce Sanitization (MEDIUM)

Consider stricter base64-only validation. The current regex allows underscore and hyphen which are base64url specific. Verify Rails nonce format.

Action Items Before Merge

  1. HIGH: Add RSpec tests for wrap_console_script_with_nonce method
  2. MEDIUM: Add RBS type signature for the new method
  3. MEDIUM: Consider stricter nonce sanitization (base64 only)
  4. LOW: Add debug logging for Rails version compatibility fallback

Summary

Solid implementation following project standards. TypeScript side is well-tested, but Ruby side needs test coverage. Once RSpec tests are added, this will be ready to merge.

Estimated effort: 30-45 minutes for tests + RBS signature.

Great work on the defense-in-depth approach! 🎉

@justin808 justin808 force-pushed the justin808/csp-nonce-console-replay branch from 4f81add to da610d4 Compare November 19, 2025 21:17
@claude
Copy link

claude bot commented Nov 19, 2025

Code Review: CSP Nonce Support for Console Replay

Summary

This PR adds Content Security Policy (CSP) nonce support for the consoleReplay script to prevent CSP violations. The implementation is well-designed with good separation of concerns between JavaScript and Ruby layers.

✅ Strengths

1. Excellent Security Approach

  • Defense-in-depth: Implements nonce sanitization at both the TypeScript layer (RenderUtils.ts:9) and relies on existing script content sanitization (scriptSanitizedVal)
  • Proper sanitization: Nonce regex [^a-zA-Z0-9+/=_-] correctly handles base64 characters while preventing attribute injection attacks
  • XSS prevention: Comprehensive test coverage for XSS attempts (buildConsoleReplay.test.js:111-125)
  • Rails CSP integration: Properly uses Rails' built-in content_security_policy_nonce helper

2. Good Cross-Version Compatibility

The Ruby implementation handles Rails version differences gracefully:

# lib/react_on_rails/helper.rb:449-456
nonce = if respond_to?(:content_security_policy_nonce)
          begin
            content_security_policy_nonce(:script)  # Rails 6.1+
          rescue ArgumentError
            content_security_policy_nonce           # Rails 5.2-6.0
          end
        end

This is a smart approach for supporting Rails 5.2-7.x.

3. Backward Compatibility

  • ✅ Existing buildConsoleReplay() still works for client-side usage
  • ✅ New getConsoleReplayScript() is additive
  • ✅ No breaking changes to API
  • ✅ CSP-unaware apps continue working (nonce is optional)

4. Good Test Coverage

  • ✅ Tests for nonce attribute presence
  • ✅ Tests for XSS/attribute injection prevention
  • ✅ Tests for empty console history edge case
  • ✅ Tests for unwrapped JavaScript output (consoleReplay())

5. Proper Documentation

  • Clear PR description explaining the implementation
  • Good inline comments explaining the security reasoning
  • Changelog entry is user-focused and comprehensive

🔍 Issues & Recommendations

1. Missing Ruby Test Coverage ⚠️

Issue: No RSpec tests for the new wrap_console_script_with_nonce method.

I searched spec/dummy/spec/helpers/react_on_rails_helper_spec.rb and found no tests for:

  • The nonce attribute being added to script tags
  • Rails CSP integration
  • Cross-version compatibility (Rails 5.2 vs 6.1+)

Recommendation: Add tests like:

describe "#wrap_console_script_with_nonce" do
  let(:helper) { PlainReactOnRailsHelper.new }
  let(:console_script) { "console.log('test');" }

  context "when CSP nonce is available" do
    before do
      allow(helper).to receive(:content_security_policy_nonce).and_return("abc123")
    end

    it "wraps script with nonce attribute" do
      result = helper.send(:wrap_console_script_with_nonce, console_script)
      expect(result).to include('nonce="abc123"')
      expect(result).to include('id="consoleReplayLog"')
    end
  end

  context "when CSP nonce is not available" do
    before do
      allow(helper).to receive(:respond_to?).with(:content_security_policy_nonce).and_return(false)
    end

    it "wraps script without nonce attribute" do
      result = helper.send(:wrap_console_script_with_nonce, console_script)
      expect(result).not_to include('nonce=')
      expect(result).to include('id="consoleReplayLog"')
    end
  end

  context "when console script is blank" do
    it "returns empty string" do
      expect(helper.send(:wrap_console_script_with_nonce, "")).to eq("")
      expect(helper.send(:wrap_console_script_with_nonce, nil)).to eq("")
    end
  end
end

2. Potential Edge Case in Nonce Sanitization 🤔

Issue: The comment in RenderUtils.ts:8 mentions preventing "attribute injection attacks", but the regex allows = which could theoretically be used in edge cases.

Current regex: /[^a-zA-Z0-9+/=_-]/g

Analysis: While CSP nonces should be base64 (which includes = for padding), a malicious nonce like abc=xyz wouldn't break out of the attribute because the surrounding quotes are controlled by the template. This is likely safe, but worth noting.

Recommendation: Document why = is explicitly allowed (base64 padding) to prevent future confusion:

// Sanitize nonce to prevent attribute injection attacks
// CSP nonces are base64 strings: alphanumeric + / + = (padding) + _ - (URL-safe variants)
const sanitizedNonce = nonce?.replace(/[^a-zA-Z0-9+/=_-]/g, '');

3. Minor: Inconsistent Method Naming 📝

Observation: The new method is getConsoleReplayScript() while the existing one is buildConsoleReplay().

The naming is actually good (get vs build indicates different responsibilities), but it might be worth calling this out in the JSDoc to be explicit about the difference.

Recommendation: Enhance JSDoc:

/**
 * Returns the console replay JavaScript code without wrapping it in script tags.
 * Unlike buildConsoleReplay(), this does NOT wrap the code in script tags,
 * allowing the caller to add CSP nonce or other attributes.
 * 
 * @returns Raw JavaScript code for console replay, or empty string if no messages
 * @see buildConsoleReplay For wrapped version with script tags
 * @internal Exported for tests and for Ruby helper to wrap with nonce
 */

4. Missing Integration Test 🧪

Issue: No end-to-end test verifying that the nonce actually makes it into the rendered HTML in a Rails view.

Recommendation: Add a system/integration test in spec/dummy/spec/helpers/react_on_rails_helper_spec.rb:

describe "CSP nonce integration", type: :system do
  it "includes nonce in console replay script when CSP is configured" do
    # Setup CSP configuration
    # Render a component with console replay
    # Verify the script tag includes the nonce attribute
  end
end

5. Pro Package Test Gap ⚠️

The Pro package helper (react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb:350) also calls wrap_console_script_with_nonce, but I didn't see corresponding tests in the diff.

Recommendation: Ensure react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb includes similar test coverage.

🔒 Security Analysis

Threat Model Coverage

XSS via script tag injection: Prevented by scriptSanitizedVal() in JS layer
XSS via nonce attribute injection: Prevented by nonce sanitization regex
CSP bypass: Properly uses Rails CSP nonce mechanism
Nonce reuse attacks: Rails handles nonce generation per-request

Remaining Considerations

  1. Nonce entropy: Relies on Rails' CSP nonce generation (should be reviewed separately if concerned)
  2. HTTPS requirement: CSP nonces are only secure over HTTPS (out of scope for this PR)

📊 Performance Impact

Minimal: The nonce sanitization is a simple regex operation on a small string (typical nonce is ~20-30 chars). No measurable performance impact expected.

🎯 Overall Assessment

This is a high-quality PR that:

  • ✅ Solves a real problem (CSP compatibility)
  • ✅ Uses defense-in-depth security principles
  • ✅ Maintains backward compatibility
  • ✅ Has good TypeScript test coverage
  • ⚠️ Needs Ruby test coverage (main blocker)
  • ✅ Is well-documented

Recommendations Summary

Before merge:

  1. REQUIRED: Add RSpec tests for wrap_console_script_with_nonce method
  2. REQUIRED: Verify Pro package has corresponding test coverage
  3. RECOMMENDED: Add integration test for end-to-end nonce flow
  4. NICE-TO-HAVE: Enhance JSDoc comments per suggestions above

After addressing the test coverage gaps, this PR should be ready to merge.


Great work on this feature! The security-conscious implementation with defense-in-depth is exactly the right approach. 🎉

@justin808 justin808 force-pushed the justin808/csp-nonce-console-replay branch from da610d4 to 52eefa4 Compare November 19, 2025 23:44
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/react-on-rails/src/RenderUtils.ts (1)

2-15: Nonce sanitization in wrapInScriptTags is appropriate for CSP

Whitelisting the nonce to [a-zA-Z0-9+/=_-] before interpolating it into nonce="..." effectively prevents attribute injection while remaining compatible with typical base64 (and base64url-style) CSP nonces. Given that callers should pass a framework-generated nonce, silently stripping unexpected characters is a reasonable defensive choice here; if you ever need diagnostics around misconfigured nonces, you could optionally log or assert when sanitizedNonce !== nonce, but that’s not required for correctness.

lib/react_on_rails/helper.rb (1)

398-401: Centralized nonce-aware console replay wrapping for SSR looks solid

Using wrap_console_script_with_nonce(console_script) in both build_react_component_result_for_server_rendered_string and _hash ensures that all SSR paths consistently emit a <script id="consoleReplayLog" nonce="..."> tag when replay_console is enabled, and nothing when it’s disabled. The helper itself:

  • Safely no-ops on blank console scripts.
  • Retrieves a CSP nonce with Rails-version-compatible content_security_policy_nonce calls.
  • Relies on content_tag for attribute escaping, and on pre-sanitized console_script_code (from the JS layer) before calling html_safe, which is consistent with existing script-escaping patterns.

This provides CSP nonce support across the main SSR flows without changing behavior when CSP is not configured.

Also applies to: 423-426, 440-464

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da610d4 and 52eefa4.

📒 Files selected for processing (8)
  • CHANGELOG.md (1 hunks)
  • lib/react_on_rails/helper.rb (5 hunks)
  • packages/react-on-rails/src/RenderUtils.ts (1 hunks)
  • packages/react-on-rails/src/base/client.ts (2 hunks)
  • packages/react-on-rails/src/buildConsoleReplay.ts (2 hunks)
  • packages/react-on-rails/src/types/index.ts (1 hunks)
  • packages/react-on-rails/tests/buildConsoleReplay.test.js (1 hunks)
  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
  • packages/react-on-rails/tests/buildConsoleReplay.test.js
  • CHANGELOG.md
  • packages/react-on-rails/src/base/client.ts
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1696
File: node_package/src/RSCPayloadContainer.ts:0-0
Timestamp: 2025-04-09T12:56:10.756Z
Learning: In the react_on_rails codebase, RSC payloads are already stringified using `JSON.stringify()` before being processed by the `escapeScript` function, which handles escaping of special characters. The function only needs to handle specific HTML markers like comments and closing script tags.
📚 Learning: 2025-04-09T12:56:10.756Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1696
File: node_package/src/RSCPayloadContainer.ts:0-0
Timestamp: 2025-04-09T12:56:10.756Z
Learning: In the react_on_rails codebase, RSC payloads are already stringified using `JSON.stringify()` before being processed by the `escapeScript` function, which handles escaping of special characters. The function only needs to handle specific HTML markers like comments and closing script tags.

Applied to files:

  • lib/react_on_rails/helper.rb
  • packages/react-on-rails/src/RenderUtils.ts
📚 Learning: 2025-07-08T05:57:29.630Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1745
File: node_package/src/RSCRequestTracker.ts:8-14
Timestamp: 2025-07-08T05:57:29.630Z
Learning: The global `generateRSCPayload` function in React on Rails Pro (RORP) is provided by the framework during rendering requests, not implemented in application code. The `declare global` statements are used to document the expected interface that RORP will inject at runtime.

Applied to files:

  • lib/react_on_rails/helper.rb
  • packages/react-on-rails/src/types/index.ts
📚 Learning: 2024-12-12T13:07:09.929Z
Learnt from: alexeyr-ci
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/ReactOnRailsRSC.ts:87-87
Timestamp: 2024-12-12T13:07:09.929Z
Learning: When handling errors in 'node_package/src/ReactOnRailsRSC.ts', include the error stack in error messages in development and test environments to aid debugging.

Applied to files:

  • packages/react-on-rails/src/types/index.ts
📚 Learning: 2025-02-12T16:38:06.537Z
Learnt from: Romex91
Repo: shakacode/react_on_rails PR: 1697
File: package-scripts.yml:28-28
Timestamp: 2025-02-12T16:38:06.537Z
Learning: The file `node_package/lib/ReactOnRails.full.js` is autogenerated during the build process and should not be present in the repository.

Applied to files:

  • packages/react-on-rails/src/types/index.ts
🧬 Code graph analysis (2)
lib/react_on_rails/helper.rb (1)
lib/react_on_rails/react_component/render_options.rb (1)
  • replay_console (82-84)
packages/react-on-rails/src/buildConsoleReplay.ts (1)
packages/react-on-rails/src/RenderUtils.ts (1)
  • wrapInScriptTags (2-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: examples (3.4, latest)
  • GitHub Check: examples (3.2, minimum)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: build-dummy-app-webpack-test-bundles (3.4, 22, latest)
  • GitHub Check: build
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: pro-lint-js-and-ruby
  • GitHub Check: claude-review
🔇 Additional comments (3)
packages/react-on-rails/src/types/index.ts (1)

439-447: Console replay internal API extension looks consistent and non-breaking

The distinction between buildConsoleReplay() (wrapped script tag) and getConsoleReplayScript() (raw JS) is clear, and the new method is added on the internal interface without changing existing signatures, which keeps this extension backward-compatible.

packages/react-on-rails/src/buildConsoleReplay.ts (1)

13-17: Nonce-aware buildConsoleReplay is wired correctly

The new consoleReplay doc clarifies that it returns bare JS, and the nonce?: string parameter on buildConsoleReplay is threaded cleanly into wrapInScriptTags with an early exit on empty history. Because the nonce is a trailing optional argument, existing call sites remain valid.

Also applies to: 57-67

lib/react_on_rails/helper.rb (1)

229-247: server_render_js now correctly uses raw JS + nonce-aware wrapper

Switching the JS snippet to ReactOnRails.getConsoleReplayScript() and then wrapping the returned code via wrap_console_script_with_nonce(console_script) (guarded by render_options.replay_console) keeps generic server_render_js aligned with the new CSP-aware console replay path. The interpolation with raw("#{html}#{console_script_tag}") behaves safely even when console_script_tag is nil or "", so there’s no functional regression here.

@justin808 justin808 force-pushed the justin808/csp-nonce-console-replay branch from 5504d5a to 8375f9c Compare November 20, 2025 04:59
@justin808 justin808 added this to the 16.2.0 milestone Nov 20, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/react-on-rails/src/RenderUtils.ts (1)

2-15: Nonce handling in wrapInScriptTags is secure and backward‑compatible

Allowlisting the nonce to base64/base64url characters before interpolating into the attribute prevents injection, and the early return for empty scriptBody preserves existing behavior. If you ever need to debug bad nonce values, you might optionally log or assert on cases where sanitizedNonce differs from the input in development.

packages/react-on-rails/src/types/index.ts (1)

439-447: Console replay API types align with the new CSP‑aware flow

The docs correctly distinguish buildConsoleReplay() (script‑tag wrapped) from getConsoleReplayScript() (raw JS), which matches the new Ruby helper usage. Since the underlying buildConsoleReplay implementation accepts optional arguments (history, skip count, nonce), you could optionally widen this method’s type to reflect those parameters if you expect external TS consumers to call it directly; otherwise keeping it as a simple no‑arg internal helper is fine.

react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb (1)

296-305: Streaming specs correctly assert script‑tag‑wrapped console replay output

The updated expectations now treat consoleReplayScript as raw JS and assert the <script id="consoleReplayLog">…</script> wrapper, with present? guards and Regexp.escape keeping the match robust. This lines up with the new helper behavior and CSP story. If you find yourself tweaking the script tag structure again, consider extracting a small shared helper in the spec to build the wrapped snippet so tests don’t duplicate the markup shape.

Also applies to: 372-383, 395-406

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5504d5a and 8375f9c.

📒 Files selected for processing (10)
  • CHANGELOG.md (1 hunks)
  • lib/react_on_rails/helper.rb (5 hunks)
  • packages/react-on-rails/src/RenderUtils.ts (1 hunks)
  • packages/react-on-rails/src/base/client.ts (2 hunks)
  • packages/react-on-rails/src/buildConsoleReplay.ts (2 hunks)
  • packages/react-on-rails/src/serverRenderReactComponent.ts (3 hunks)
  • packages/react-on-rails/src/types/index.ts (1 hunks)
  • packages/react-on-rails/tests/buildConsoleReplay.test.js (1 hunks)
  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb (1 hunks)
  • react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/react-on-rails/tests/buildConsoleReplay.test.js
  • packages/react-on-rails/src/base/client.ts
  • react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb
  • CHANGELOG.md
  • packages/react-on-rails/src/serverRenderReactComponent.ts
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1696
File: node_package/src/RSCPayloadContainer.ts:0-0
Timestamp: 2025-04-09T12:56:10.756Z
Learning: In the react_on_rails codebase, RSC payloads are already stringified using `JSON.stringify()` before being processed by the `escapeScript` function, which handles escaping of special characters. The function only needs to handle specific HTML markers like comments and closing script tags.
📚 Learning: 2025-04-09T12:56:10.756Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1696
File: node_package/src/RSCPayloadContainer.ts:0-0
Timestamp: 2025-04-09T12:56:10.756Z
Learning: In the react_on_rails codebase, RSC payloads are already stringified using `JSON.stringify()` before being processed by the `escapeScript` function, which handles escaping of special characters. The function only needs to handle specific HTML markers like comments and closing script tags.

Applied to files:

  • packages/react-on-rails/src/RenderUtils.ts
  • lib/react_on_rails/helper.rb
📚 Learning: 2025-07-08T05:57:29.630Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1745
File: node_package/src/RSCRequestTracker.ts:8-14
Timestamp: 2025-07-08T05:57:29.630Z
Learning: The global `generateRSCPayload` function in React on Rails Pro (RORP) is provided by the framework during rendering requests, not implemented in application code. The `declare global` statements are used to document the expected interface that RORP will inject at runtime.

Applied to files:

  • packages/react-on-rails/src/RenderUtils.ts
  • packages/react-on-rails/src/types/index.ts
  • lib/react_on_rails/helper.rb
📚 Learning: 2024-12-12T13:07:09.929Z
Learnt from: alexeyr-ci
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/ReactOnRailsRSC.ts:87-87
Timestamp: 2024-12-12T13:07:09.929Z
Learning: When handling errors in 'node_package/src/ReactOnRailsRSC.ts', include the error stack in error messages in development and test environments to aid debugging.

Applied to files:

  • packages/react-on-rails/src/types/index.ts
📚 Learning: 2025-02-12T16:38:06.537Z
Learnt from: Romex91
Repo: shakacode/react_on_rails PR: 1697
File: package-scripts.yml:28-28
Timestamp: 2025-02-12T16:38:06.537Z
Learning: The file `node_package/lib/ReactOnRails.full.js` is autogenerated during the build process and should not be present in the repository.

Applied to files:

  • packages/react-on-rails/src/types/index.ts
  • react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb
📚 Learning: 2025-04-26T21:55:55.874Z
Learnt from: alexeyr-ci2
Repo: shakacode/react_on_rails PR: 1732
File: spec/dummy/client/app-react16/startup/ReduxSharedStoreApp.client.jsx:40-44
Timestamp: 2025-04-26T21:55:55.874Z
Learning: In the react_on_rails project, files under `app-react16` directories are copied/moved to corresponding `/app` directories during the conversion process (removing the `-react16` suffix), which affects their relative import paths at runtime.

Applied to files:

  • react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb
📚 Learning: 2024-10-08T20:53:47.076Z
Learnt from: theforestvn88
Repo: shakacode/react_on_rails PR: 1620
File: spec/dummy/client/app/startup/HelloTurboStream.jsx:3-3
Timestamp: 2024-10-08T20:53:47.076Z
Learning: The `RailsContext` import in `spec/dummy/client/app/startup/HelloTurboStream.jsx` is used later in the project, as clarified by the user theforestvn88.

Applied to files:

  • react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.

Applied to files:

  • react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb
📚 Learning: 2025-09-16T08:01:11.146Z
Learnt from: justin808
Repo: shakacode/react_on_rails PR: 1770
File: lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx:2-2
Timestamp: 2025-09-16T08:01:11.146Z
Learning: React on Rails uses webpack CSS Modules configuration with namedExports: true, which requires the import syntax `import * as style from './file.module.css'` rather than the default export pattern. This configuration enables better tree shaking and bundle size optimization for CSS modules.

Applied to files:

  • react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb
🧬 Code graph analysis (3)
react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb (1)
react_on_rails_pro/spec/react_on_rails_pro/stream_spec.rb (1)
  • include (7-31)
lib/react_on_rails/helper.rb (1)
lib/react_on_rails/react_component/render_options.rb (1)
  • replay_console (82-84)
packages/react-on-rails/src/buildConsoleReplay.ts (1)
packages/react-on-rails/src/RenderUtils.ts (1)
  • wrapInScriptTags (2-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: build-dummy-app-webpack-test-bundles (3.4, 22, latest)
  • GitHub Check: examples (3.4, latest)
  • GitHub Check: examples (3.2, minimum)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: pro-lint-js-and-ruby
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: build
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: claude-review
🔇 Additional comments (3)
packages/react-on-rails/src/buildConsoleReplay.ts (1)

13-17: consoleReplay / buildConsoleReplay split and nonce threading look good

Exporting consoleReplay as the raw JS generator and having the default buildConsoleReplay wrap it in <script id="consoleReplayLog">…</script> with an optional nonce cleanly supports both CSP‑aware server usage and existing consumers. Adding the nonce as a trailing optional parameter keeps prior call sites intact while centralizing sanitization in wrapInScriptTags.

Also applies to: 57-67

lib/react_on_rails/helper.rb (2)

229-248: Nonce‑aware console replay wiring in helpers is coherent and safe

Switching server_render_js to use ReactOnRails.getConsoleReplayScript() (raw JS) and then wrapping via wrap_console_script_with_nonce aligns this path with the rest of the CSP‑aware flow. The new helper cleanly handles:

  • ignoring blank console scripts,
  • obtaining a nonce via content_security_policy_nonce across Rails versions (directive argument where supported, rescuing ArgumentError otherwise), and
  • emitting <script id="consoleReplayLog" nonce="…">…</script> when a nonce is present.

Given that the JS side already sanitizes console replay payloads before they reach Ruby, using html_safe here is consistent with existing patterns in the project.

Also applies to: 440-464


379-402: react_component result builders now correctly emit nonce‑wrapped console replay scripts

Both build_react_component_result_for_server_rendered_string and _hash now derive result_console_script via wrap_console_script_with_nonce(console_script) when render_options.replay_console is true, otherwise they pass an empty string into compose_react_component_html_with_spec_and_console. This keeps the console replay script optional, centralizes CSP nonce handling in one helper, and preserves the existing HTML composition behavior.

Also applies to: 406-438

@justin808 justin808 force-pushed the justin808/csp-nonce-console-replay branch from 8375f9c to ae833ad Compare November 20, 2025 05:41
@claude
Copy link

claude bot commented Nov 20, 2025

Code Review: CSP Nonce Support for Console Replay

Thank you for this well-implemented PR! This is a solid security enhancement. Here's my detailed review:


Strengths

1. Excellent Security Design

  • Defense-in-depth approach: Nonce sanitization happens in BOTH TypeScript (RenderUtils.ts:9) AND Ruby would rely on Rails' built-in sanitization via content_tag
  • Cross-version compatibility: Thoughtful handling of Rails 5.2-7.x differences in CSP nonce API (helper.rb:447-456)
  • XSS prevention: Good test coverage for attribute injection attacks (buildConsoleReplay.test.js:111-124)

2. Well-Structured Architecture

  • Clear separation of concerns: JavaScript returns raw code via getConsoleReplayScript(), Ruby handles wrapping with nonce
  • Backward compatibility: Existing buildConsoleReplay() method preserved, new method is additive
  • Consistent pattern: Applied across both open-source and Pro packages

3. Comprehensive Testing

  • TypeScript tests cover: nonce inclusion, XSS sanitization, empty script handling
  • Pro helper specs updated to match new wrapped format
  • Good use of regex matching in Pro specs for flexibility

4. Good Documentation

  • Clear comments explaining sanitization rationale
  • Updated CHANGELOG with appropriate detail
  • Type definitions updated with JSDoc comments

🔍 Areas for Improvement

1. Missing RSpec Tests for Core Helper Method ⚠️

The new wrap_console_script_with_nonce method in lib/react_on_rails/helper.rb lacks direct unit tests. While Pro specs test the integration, the open-source gem should have tests covering:

# Suggested test cases for spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
describe '#wrap_console_script_with_nonce' do
  it 'wraps script with nonce when CSP is configured'
  it 'returns script without nonce when CSP is not configured'
  it 'returns empty string when script code is blank'
  it 'handles Rails 5.2-6.0 (no-argument nonce method)'
  it 'handles Rails 6.1+ (with-argument nonce method)'
  it 'includes id="consoleReplayLog" attribute'
end

Why this matters: The cross-version Rails compatibility logic (helper.rb:449-456) is critical and should be tested to ensure it works across all supported Rails versions.

2. Potential Edge Case: Nonce Sanitization Consistency

The TypeScript sanitization regex (RenderUtils.ts:9) allows [a-zA-Z0-9+/=_-], but:

  • CSP nonces are typically base64-encoded, so +/= are expected
  • The - and _ suggest base64url encoding support (good!)
  • However, the comment only mentions base64, not base64url

Suggestion: Update the comment to clarify both formats are supported:

// CSP nonces are base64 or base64url strings, so allow: alphanumeric, +, /, =, -, _

3. Documentation: Security Implications

The comment in helper.rb:441-442 states the content is "already sanitized by scriptSanitizedVal()", but:

  • scriptSanitizedVal() is referenced but not visible in the diff
  • Reviewers unfamiliar with the codebase might not trust this claim

Suggestion: Add a comment linking to where scriptSanitizedVal() is defined, or briefly explain what it does:

# The console_script_code is already sanitized by scriptSanitizedVal() 
# in buildConsoleReplay.ts (escapes special chars for safe eval in browser)

4. TypeScript Test Expectation Clarity

In buildConsoleReplay.test.js:119, the comment says:

// = is kept as it's valid in base64, but the quotes are stripped making it harmless

But the test expects nonce="abc123onload=alert1". The = character IS kept (as noted), which could be slightly confusing. Consider clarifying:

// Should strip dangerous characters (quotes, parens, spaces)
// The '=' is kept because it's valid in base64, but without quotes it can't break out of the attribute

🐛 Potential Issues

1. Bug Fix Also Included

The PR fixes a bug (consoleLogScriptconsoleReplayScript in helper.rb:245). This is good but should be highlighted more prominently in the PR description summary section, as it's easy to miss.

2. Performance: ArgumentError Exception Handling

The Rails version compatibility code (helper.rb:450-455) uses exception handling for control flow:

begin
  content_security_policy_nonce(:script)
rescue ArgumentError
  content_security_policy_nonce
end

Concern: This will raise/catch an exception on EVERY render in Rails 5.2-6.0 apps. While not a critical performance issue (SSR is expensive anyway), it's suboptimal.

Better approach:

# One-time version check (could be in an initializer or memoized)
nonce = if respond_to?(:content_security_policy_nonce)
          # Check Rails version or method arity instead of rescuing
          if method(:content_security_policy_nonce).arity == -1 # accepts optional args
            content_security_policy_nonce(:script)
          else
            content_security_policy_nonce
          end
        end

However, the current approach is safe and correct, so this is a minor optimization opportunity, not a blocker.


🔒 Security Analysis

XSS Protection: Strong

  • Nonce sanitization in TypeScript prevents attribute injection
  • Rails' content_tag provides additional escaping
  • Tests verify malicious input is neutralized

CSP Compliance: Correct

  • Nonce attribute is added conditionally (only when CSP is configured)
  • Follows Rails' CSP conventions
  • Works with both script-src 'self' and script-src 'nonce-...' 'strict-dynamic' policies

⚠️ Dependency on JavaScript Sanitization

The Ruby code trusts that JavaScript has already sanitized the console script content via scriptSanitizedVal(). This is fine IF:

  1. That sanitization is robust (should verify if not already reviewed)
  2. The contract between JS and Ruby layers is clear

Recommendation: Add a defensive assertion or comment noting this dependency.


📊 Test Coverage Assessment

Component Coverage Notes
TypeScript nonce sanitization ✅ Excellent 3 test cases cover happy path, XSS, edge cases
TypeScript consoleReplay() ✅ Good Tests verify no script tags returned
Ruby wrap_console_script_with_nonce ⚠️ Missing No direct unit tests
Pro integration ✅ Good Updated specs verify wrapped format
Rails version compatibility Not tested Critical cross-version logic untested

🎯 Recommendations

Must-Have Before Merge:

  1. Add RSpec tests for wrap_console_script_with_nonce covering Rails version compatibility

Nice-to-Have:

  1. Update TypeScript comment to mention base64url encoding
  2. Consider optimizing Rails version detection to avoid exception handling
  3. Add comment linking to scriptSanitizedVal() implementation

Follow-Up (Post-Merge):

  1. Consider adding integration test with actual CSP headers configured
  2. Document CSP setup steps for users in README or guides

Overall Assessment

This is high-quality, production-ready code with excellent security practices. The architecture is clean, backward compatibility is preserved, and the implementation is thoughtful.

The main gap is test coverage for the Ruby helper method, particularly the Rails version compatibility logic. Once that's addressed, this is ready to merge.

Approval Status: ✅ Approve with minor changes (add RSpec tests)


Great work on this security enhancement! 🎉

justin808 and others added 7 commits November 19, 2025 20:52
Fixes #1930

- Add optional nonce parameter to wrapInScriptTags() and buildConsoleReplay()
- Add getConsoleReplayScript() method to return JS without script tags
- Update Ruby helper to wrap console script with CSP nonce using content_security_policy_nonce(:script)
- Fix bug: change consoleLogScript to consoleReplayScript in server_render_js
- Pro helper now also wraps console scripts with nonce

This allows the consoleReplay script to work with Content Security Policy
without violations when using script-src :self or similar policies.

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

Co-Authored-By: Claude <[email protected]>
- Add TypeScript tests for:
  - buildConsoleReplay() with nonce parameter
  - consoleReplay() returns JS without script tags
  - Empty string handling with nonce

- Add security comments explaining:
  - Why html_safe is safe (content pre-sanitized via scriptSanitizedVal)
  - CSP nonce availability (Rails 5.2+)

- Add blank line before wrap_console_script_with_nonce method for consistency

All tests passing (100 passed in react-on-rails, 30 passed in pro).

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

Co-Authored-By: Claude <[email protected]>
The content_security_policy_nonce helper has different signatures across Rails versions:
- Rails 5.2-6.0: content_security_policy_nonce (no arguments)
- Rails 6.1+: content_security_policy_nonce(directive) (optional argument)

This change adds a try-catch to handle both versions gracefully.

Fixes CI failures with "wrong number of arguments (given 1, expected 0)".

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

Co-Authored-By: Claude <[email protected]>
Security improvements:
- Sanitize nonce values to prevent attribute injection attacks
- Only allow base64-safe characters: alphanumeric, +, /, =, -, _
- Add test to verify malicious nonce values are sanitized
- Document the security measure in code comments

Even though Rails content_security_policy_nonce() returns safe values,
this adds defense-in-depth by sanitizing at the JavaScript layer.

All tests passing (101 passed).

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

Co-Authored-By: Claude <[email protected]>
Document the new CSP nonce support feature in CHANGELOG.md following
project conventions. Entry includes implementation details and PR reference.

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

Co-Authored-By: Claude <[email protected]>
The issue was that serverRenderReactComponent was calling buildConsoleReplay()
which wraps the console code in script tags, but Ruby was ALSO wrapping it
with wrap_console_script_with_nonce(), resulting in nested script tags like:
<script id="consoleReplayLog"><script>...</script></script>

Changed serverRenderReactComponent to call consoleReplay() instead, which
returns just the JavaScript code without script tags. Ruby then wraps it
once with the proper nonce attribute.

This fixes the failing tests:
- ReactOnRailsProHelper html_streaming_react_component creates a fiber
- Console logging from server has server log messages in the script

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

Co-Authored-By: Claude <[email protected]>
Simplified the if/else blocks to avoid RuboCop Layout cop bugs.
The Layout cops were crashing on multi-line if/else assignments
even though the code had no actual offenses.

Changed to simpler variable assignments to avoid the crashes.

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

Co-Authored-By: Claude <[email protected]>
@justin808 justin808 force-pushed the justin808/csp-nonce-console-replay branch from ed27367 to 0740ba6 Compare November 20, 2025 06:53
@justin808
Copy link
Member Author

@claude review PR

@claude
Copy link

claude bot commented Nov 20, 2025

Claude finished @justin808's task —— View job


PR Review: Add CSP nonce support for consoleReplay script

Todo List

  • Read CLAUDE.md for repo guidelines
  • Review TypeScript changes (RenderUtils.ts, buildConsoleReplay.ts, types)
  • Review Ruby changes (helper.rb, Pro helper)
  • Analyze security implementation
  • Check test coverage
  • Verify CLAUDE.md compliance (RuboCop, changelog, etc.)
  • Compile and post comprehensive review feedback

Overall Assessment

This is an excellent, production-ready PR that adds important CSP nonce support for the console replay script. The implementation demonstrates strong security practices, thoughtful API design, and comprehensive testing. I found no blocking issues.

Rating: 9.5/10 - Ready to merge after addressing one minor enhancement opportunity.


✅ Strengths

1. Outstanding Security Implementation

The defense-in-depth approach is exemplary:

  • Nonce sanitization (RenderUtils.ts:9): The regex [^a-zA-Z0-9+/=_-] correctly allows base64/base64url characters while preventing attribute injection
  • Content sanitization: JavaScript already sanitizes via scriptSanitizedVal() before Ruby receives it
  • Rails helper safety: Uses content_tag which auto-escapes attributes
  • XSS test coverage: Test at line 111-125 explicitly verifies malicious input is neutralized

The comment on helper.rb:441-442 correctly documents why html_safe is secure here.

2. Excellent Cross-Version Rails Compatibility

The Rails version handling (helper.rb:449-456) is sophisticated:

begin
  content_security_policy_nonce(:script)  # Rails 6.1+
rescue ArgumentError
  content_security_policy_nonce           # Rails 5.2-6.0
end

This gracefully supports Rails 5.2-7.x without requiring version checks. Smart use of exception handling for API differences.

3. Clean API Design

The separation of concerns is perfect:

  • TypeScript layer: New getConsoleReplayScript() returns raw JS (no script tags)
  • Ruby layer: Wraps with Rails-specific CSP nonce
  • Backward compatibility: Existing buildConsoleReplay() still works
  • Optional nonce parameter: Trailing optional argument preserves existing call sites

The naming distinction (get vs build) clearly signals different responsibilities.

4. Comprehensive Test Coverage

TypeScript tests (buildConsoleReplay.test.js:79-125) cover all key scenarios:

  • ✅ Nonce attribute inclusion (line 79-86)
  • ✅ Empty console history edge case (line 88-93)
  • ✅ Script tag-less output from consoleReplay() (line 95-109)
  • ✅ XSS prevention via nonce sanitization (line 111-125)

Pro package specs (react_on_rails_pro_helper_spec.rb:372-406) verify the wrapped format in streaming contexts.

5. Documentation & Project Compliance

  • CHANGELOG entry properly formatted with PR link and attribution (line 28)
  • JSDoc comments clearly explain the new method's purpose (types/index.ts:439-447)
  • Code comments document security assumptions (helper.rb:441-442, RenderUtils.ts:7-9)
  • PR description thorough and explains the "why" behind the implementation

🔍 Areas for Enhancement

1. Missing Unit Tests for Ruby Helper Method (Minor)

The new wrap_console_script_with_nonce method (helper.rb:443-464) lacks direct unit tests in the open-source gem's spec files. While the Pro package specs test the integration, the core helper should have standalone tests covering:

Suggested test cases for spec/dummy/spec/helpers/react_on_rails_helper_spec.rb:

describe '#wrap_console_script_with_nonce' do
  let(:helper) { Class.new { include ReactOnRailsHelper }.new }
  let(:console_script) { "console.log.apply(console, ['test']);" }
  
  context 'when CSP nonce is available' do
    before do
      allow(helper).to receive(:content_security_policy_nonce)
        .with(:script).and_return('abc123')
    end
    
    it 'wraps script with nonce attribute' do
      result = helper.send(:wrap_console_script_with_nonce, console_script)
      expect(result).to include('nonce="abc123"')
      expect(result).to include('id="consoleReplayLog"')
      expect(result).to include(console_script)
    end
  end
  
  context 'when CSP is not configured' do
    before do
      allow(helper).to receive(:respond_to?)
        .with(:content_security_policy_nonce).and_return(false)
    end
    
    it 'wraps script without nonce attribute' do
      result = helper.send(:wrap_console_script_with_nonce, console_script)
      expect(result).not_to include('nonce=')
      expect(result).to include('id="consoleReplayLog"')
    end
  end
  
  context 'with Rails 5.2-6.0 compatibility' do
    before do
      allow(helper).to receive(:content_security_policy_nonce)
        .with(:script).and_raise(ArgumentError)
      allow(helper).to receive(:content_security_policy_nonce)
        .with(no_args).and_return('fallback123')
    end
    
    it 'falls back to no-argument method' do
      result = helper.send(:wrap_console_script_with_nonce, console_script)
      expect(result).to include('nonce="fallback123"')
    end
  end
  
  context 'with blank input' do
    it 'returns empty string' do
      expect(helper.send(:wrap_console_script_with_nonce, '')).to eq('')
      expect(helper.send(:wrap_console_script_with_nonce, nil)).to eq('')
    end
  end
end

Why this matters: The cross-version Rails compatibility logic is critical infrastructure code that should be tested to prevent regressions across Rails 5.2-7.x.

Priority: Nice-to-have. The integration tests in Pro specs provide good coverage, but standalone unit tests would improve maintainability.


🎯 Technical Analysis

Security Assessment: EXCELLENT

Threat Model Coverage:

  • ✅ Script tag injection: Prevented by scriptSanitizedVal() in JS layer
  • ✅ Attribute injection: Prevented by nonce sanitization regex
  • ✅ CSP bypass: Properly uses Rails CSP nonce mechanism
  • ✅ XSS via console content: Pre-sanitized before reaching Ruby
  • ✅ Nonce reuse: Rails handles per-request nonce generation

No security vulnerabilities identified.

Performance Impact: NEGLIGIBLE

  • One regex operation on a ~20-30 character string (nonce)
  • One Rails helper call (memoized by Rails)
  • No database queries or I/O
  • Early returns for empty console history

Backward Compatibility: PERFECT

  • ✅ CSP-unaware apps: Empty nonce works as before
  • ✅ Existing API: buildConsoleReplay() preserved
  • ✅ New API: getConsoleReplayScript() is additive
  • ✅ Optional parameters: Trailing optional args don't break existing calls

Code Quality: EXCELLENT

  • Clear variable names (sanitizedNonce, nonceAttr, console_script_tag)
  • Proper error handling (Rails version fallback)
  • Good separation of concerns (JS returns code, Ruby wraps)
  • Consistent patterns (ternary operators for optional wrapping)

📝 Detailed Review by File

packages/react-on-rails/src/RenderUtils.ts

Lines 2-16: Clean implementation of nonce-aware wrapper.

Strengths:

  • Early return for empty body prevents empty tags
  • Nonce sanitization regex is correct for base64/base64url
  • Comment explains the security rationale

💡 Enhancement opportunity: The comment mentions "base64 strings" but the regex also allows _ and - which are base64url specific. Consider clarifying:

// CSP nonces should be base64 or base64url strings, so only allow: alphanumeric, +, /, =, -, and _

packages/react-on-rails/src/buildConsoleReplay.ts

Lines 13-17: Excellent separation between raw JS generator and wrapped version.

Strengths:

  • JSDoc clearly documents @internal usage
  • consoleReplay() exported for Ruby consumption
  • Nonce parameter threaded cleanly

Lines 57-67: Default export maintains backward compatibility while adding nonce support.

lib/react_on_rails/helper.rb

Lines 229-248: Perfect integration of new API in server_render_js.

Strengths:

  • Switched from buildConsoleReplay() to getConsoleReplayScript()
  • Conditional wrapping respects replay_console option
  • Proper nil handling (postfix if leaves variable uninitialized if false, but interpolation handles it)

Lines 398-401, 423-426: Consistent ternary pattern ensures strings (not nil).

Strengths:

  • Uses ternary operator: replay_console ? wrap_console_script_with_nonce(console_script) : ""
  • Prevents "nil" string in output

Lines 440-464: The star of the show - excellent helper method.

Strengths:

  • Blank input guard (line 444)
  • Rails version compatibility with exception handling (449-456)
  • Correct use of content_tag for attribute escaping
  • Well-documented security assumption (441-442)
  • Proper use of html_safe given pre-sanitization

Note: Using rescue ArgumentError for control flow is slightly inelegant but pragmatic. Checking method arity would be cleaner but this approach is simpler and correct:

# Alternative (more complex, not necessary):
if method(:content_security_policy_nonce).arity == -1
  content_security_policy_nonce(:script)
else
  content_security_policy_nonce
end

Current approach is fine - exception handling overhead is negligible in SSR context.

packages/react-on-rails/tests/buildConsoleReplay.test.js

Lines 79-125: Comprehensive test suite.

Strengths:

  • Tests nonce inclusion (79-86)
  • Tests empty console edge case (88-93)
  • Tests unwrapped output (95-109)
  • Tests XSS prevention (111-125) - this is particularly valuable

The XSS test demonstrates that 'abc123" onload="alert(1)' becomes 'abc123onload=alert1', effectively neutralizing the attack.

CHANGELOG.md

Line 28: Perfect changelog entry.

Strengths:

  • User-facing description
  • Explains the "why" (CSP violations)
  • Notes cross-version compatibility
  • Links to PR and author
  • Proper formatting per CLAUDE.md guidelines

🔒 Comparison with CodeRabbit Reviews

I reviewed the extensive CodeRabbit feedback in the PR. Key points:

  1. XSS concerns in RenderUtils.ts: CodeRabbit initially flagged the nonce interpolation as unsafe. This was addressed by adding sanitization in commit 0046401. ✅ Resolved.

  2. Nil handling in helper.rb:246: CodeRabbit noted that postfix if could leave console_script_tag nil. This was fixed by using ternary operators in commits addressing the other rendering paths. ✅ Resolved.

  3. Ruby test coverage: CodeRabbit and subsequent reviews noted missing RSpec tests. This remains the only outstanding enhancement opportunity from my review as well.

All critical issues raised by automated reviews have been addressed. The implementation is solid.


🎓 What I Learned from This PR

This PR demonstrates several best practices worth highlighting:

  1. Defense-in-depth security: Multiple layers (JS sanitization + nonce sanitization + Rails escaping)
  2. Graceful degradation: Works with and without CSP configuration
  3. Cross-version compatibility: Handles API differences without version checks
  4. Test-driven security: XSS test explicitly verifies sanitization works
  5. Clear API boundaries: TypeScript returns data, Ruby handles framework integration

📋 Pre-Merge Checklist

Based on CLAUDE.md requirements:

  • RuboCop compliance: PR description states linting passed
  • Trailing newlines: No issues noted in reviews
  • CHANGELOG entry: Present and properly formatted
  • Test coverage: TypeScript tests comprehensive; Ruby tests could be enhanced
  • Documentation: JSDoc and inline comments present
  • Backward compatibility: Fully maintained
  • Security review: Multiple layers of protection verified

🚀 Recommendation

APPROVE AND MERGE

This PR is production-ready. The missing Ruby unit tests are a "nice-to-have" enhancement, not a blocker. The integration tests in the Pro package provide adequate coverage, and the implementation is correct.

Optional Follow-Up (Post-Merge)

If time permits, consider adding the Ruby unit tests suggested above to improve long-term maintainability and make the Rails version compatibility logic easier to verify during future Rails upgrades.


Summary

Excellent work on this security enhancement! The implementation demonstrates:

  • Strong security practices with defense-in-depth
  • Thoughtful API design with clear separation of concerns
  • Comprehensive test coverage (TypeScript side)
  • Perfect backward compatibility
  • Cross-version Rails support (5.2-7.x)

The CSP nonce support will prevent violations for users with restrictive policies, and the implementation is robust and well-tested. This is high-quality, production-ready code. 🎉


justin808 and others added 4 commits November 19, 2025 21:37
The test was unconditionally removing the first line assuming there
would be a leading blank line from the old wrapInScriptTags format.

With the new approach where Ruby wraps the console script using
content_tag (via wrap_console_script_with_nonce), there's no leading
blank line, so the test was incorrectly skipping the first log line.

Changed to only shift if the first line is actually empty.

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

Co-Authored-By: Claude <[email protected]>
The test page renders two ReduxSharedStoreApp components with
trace: true, so both log their render messages. Update expected
output to include the second component's console log.

Related to CSP nonce console replay changes.
When multiple React components with replay_console are rendered on the same page,
each creates its own <script id="consoleReplayLog"> tag. Nokogiri's .text method
concatenates text from all matching elements without separators, causing console
log statements to be joined without newlines.

Fixed by explicitly mapping over all script tags and joining their text with
newlines before parsing.

This fix avoids adding runtime complexity or requiring users to call new helper
methods. The existing behavior (multiple script tags per component) is preserved.

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

Co-Authored-By: Claude <[email protected]>
Adds comprehensive unit tests for the wrap_console_script_with_nonce helper method
to ensure proper CSP nonce handling across different Rails versions.

Test coverage includes:
- CSP nonce attribute generation when available
- Script tag creation without nonce when CSP is not configured
- Rails 5.2-6.0 compatibility fallback (ArgumentError handling)
- Blank input handling (empty, nil, whitespace)
- Multi-line script preservation (newlines maintained)
- Special character escaping in script content

These tests validate the cross-version Rails compatibility logic (5.2-7.x) that is
critical infrastructure code for the CSP nonce support feature.

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

Co-Authored-By: Claude <[email protected]>
@justin808
Copy link
Member Author

@claude review this pr

@claude
Copy link

claude bot commented Nov 21, 2025

Claude finished @justin808's task —— View job


Code Review: CSP Nonce Support for consoleReplay Script

Overall Assessment

This is an excellent, production-ready PR that adds important CSP nonce support for the console replay script. The implementation demonstrates strong security practices, thoughtful API design, and comprehensive testing. I found no blocking issues.

Rating: 9.5/10 - Ready to merge after addressing one minor enhancement opportunity.


✅ Strengths

1. Outstanding Security Implementation

The defense-in-depth approach is exemplary:

  • Nonce sanitization (RenderUtils.ts:9): The regex [^a-zA-Z0-9+/=_-] correctly allows base64/base64url characters while preventing attribute injection
  • Content sanitization: JavaScript already sanitizes via scriptSanitizedVal() before Ruby receives it
  • Rails helper safety: Uses content_tag which auto-escapes attributes
  • XSS test coverage: Test at line 111-125 explicitly verifies malicious input is neutralized

The comment on helper.rb:441-442 correctly documents why html_safe is secure here.

2. Excellent Cross-Version Rails Compatibility

The Rails version handling (helper.rb:449-456) is sophisticated:

begin
  content_security_policy_nonce(:script)  # Rails 6.1+
rescue ArgumentError
  content_security_policy_nonce           # Rails 5.2-6.0
end

This gracefully supports Rails 5.2-7.x without requiring version checks. Smart use of exception handling for API differences.

3. Clean API Design

The separation of concerns is perfect:

  • TypeScript layer: New getConsoleReplayScript() returns raw JS (no script tags)
  • Ruby layer: Wraps with Rails-specific CSP nonce
  • Backward compatibility: Existing buildConsoleReplay() still works
  • Optional nonce parameter: Trailing optional argument preserves existing call sites

The naming distinction (get vs build) clearly signals different responsibilities.

4. Comprehensive Test Coverage

TypeScript tests (buildConsoleReplay.test.js:79-125) cover all key scenarios:

  • ✅ Nonce attribute inclusion (line 79-86)
  • ✅ Empty console history edge case (line 88-93)
  • ✅ Script tag-less output from consoleReplay() (line 95-109)
  • ✅ XSS prevention via nonce sanitization (line 111-125)

Ruby tests (react_on_rails_helper_spec.rb:755-872) provide excellent coverage:

  • ✅ CSP nonce inclusion when available
  • ✅ Graceful fallback when CSP is not configured
  • ✅ Rails 5.2-6.0 compatibility (ArgumentError fallback)
  • ✅ Blank/nil input handling
  • ✅ Multi-line script preservation
  • ✅ Special character handling

Pro package specs (react_on_rails_pro_helper_spec.rb:372-406) verify the wrapped format in streaming contexts.

5. Documentation & Project Compliance

  • CHANGELOG entry properly formatted with PR link and attribution (line 28)
  • JSDoc comments clearly explain the new method's purpose (types/index.ts:439-447)
  • Code comments document security assumptions (helper.rb:441-442, RenderUtils.ts:7-9)
  • PR description thorough and explains the "why" behind the implementation

🔍 Areas for Enhancement

1. Missing Unit Tests for Ruby Helper Method (Minor)

The new wrap_console_script_with_nonce method (helper.rb:443-464) lacks direct unit tests in the open-source gem's spec files. While the Pro package specs test the integration, the core helper should have standalone tests covering:

Suggested test cases for spec/dummy/spec/helpers/react_on_rails_helper_spec.rb:

describe '#wrap_console_script_with_nonce' do
  let(:helper) { Class.new { include ReactOnRailsHelper }.new }
  let(:console_script) { "console.log.apply(console, ['test']);" }
  
  context 'when CSP nonce is available' do
    before do
      allow(helper).to receive(:content_security_policy_nonce)
        .with(:script).and_return('abc123')
    end
    
    it 'wraps script with nonce attribute' do
      result = helper.send(:wrap_console_script_with_nonce, console_script)
      expect(result).to include('nonce="abc123"')
      expect(result).to include('id="consoleReplayLog"')
      expect(result).to include(console_script)
    end
  end
  
  context 'when CSP is not configured' do
    before do
      allow(helper).to receive(:respond_to?)
        .with(:content_security_policy_nonce).and_return(false)
    end
    
    it 'wraps script without nonce attribute' do
      result = helper.send(:wrap_console_script_with_nonce, console_script)
      expect(result).not_to include('nonce=')
      expect(result).to include('id="consoleReplayLog"')
    end
  end
  
  context 'with Rails 5.2-6.0 compatibility' do
    before do
      allow(helper).to receive(:content_security_policy_nonce)
        .with(:script).and_raise(ArgumentError)
      allow(helper).to receive(:content_security_policy_nonce)
        .with(no_args).and_return('fallback123')
    end
    
    it 'falls back to no-argument method' do
      result = helper.send(:wrap_console_script_with_nonce, console_script)
      expect(result).to include('nonce="fallback123"')
    end
  end
  
  context 'with blank input' do
    it 'returns empty string' do
      expect(helper.send(:wrap_console_script_with_nonce, '')).to eq('')
      expect(helper.send(:wrap_console_script_with_nonce, nil)).to eq('')
    end
  end
end

Why this matters: The cross-version Rails compatibility logic is critical infrastructure code that should be tested to prevent regressions across Rails 5.2-7.x.

Priority: Nice-to-have. The integration tests in Pro specs provide good coverage, but standalone unit tests would improve maintainability.

UPDATE: I see these tests were already added in spec/dummy/spec/helpers/react_on_rails_helper_spec.rb:755-872! Excellent work on comprehensive test coverage. This concern is fully addressed. ✅


🎯 Technical Analysis

Security Assessment: EXCELLENT

Threat Model Coverage:

  • ✅ Script tag injection: Prevented by scriptSanitizedVal() in JS layer
  • ✅ Attribute injection: Prevented by nonce sanitization regex
  • ✅ CSP bypass: Properly uses Rails CSP nonce mechanism
  • ✅ XSS via console content: Pre-sanitized before reaching Ruby
  • ✅ Nonce reuse: Rails handles per-request nonce generation

No security vulnerabilities identified.

Performance Impact: NEGLIGIBLE

  • One regex operation on a ~20-30 character string (nonce)
  • One Rails helper call (memoized by Rails)
  • No database queries or I/O
  • Early returns for empty console history

Backward Compatibility: PERFECT

  • ✅ CSP-unaware apps: Empty nonce works as before
  • ✅ Existing API: buildConsoleReplay() preserved
  • ✅ New API: getConsoleReplayScript() is additive
  • ✅ Optional parameters: Trailing optional args don't break existing calls

Code Quality: EXCELLENT

  • Clear variable names (sanitizedNonce, nonceAttr, console_script_tag)
  • Proper error handling (Rails version fallback)
  • Good separation of concerns (JS returns code, Ruby wraps)
  • Consistent patterns (ternary operators for optional wrapping)

📝 Detailed Review by File

packages/react-on-rails/src/RenderUtils.ts

Lines 2-16: Clean implementation of nonce-aware wrapper.

Strengths:

  • Early return for empty body prevents empty tags
  • Nonce sanitization regex is correct for base64/base64url
  • Comment explains the security rationale

💡 Enhancement opportunity: The comment mentions "base64 strings" but the regex also allows _ and - which are base64url specific. Consider clarifying:

// CSP nonces should be base64 or base64url strings, so only allow: alphanumeric, +, /, =, -, and _

packages/react-on-rails/src/buildConsoleReplay.ts

Lines 13-17: Excellent separation between raw JS generator and wrapped version.

Strengths:

  • JSDoc clearly documents @internal usage
  • consoleReplay() exported for Ruby consumption
  • Nonce parameter threaded cleanly

Lines 57-67: Default export maintains backward compatibility while adding nonce support.

lib/react_on_rails/helper.rb

Lines 229-248: Perfect integration of new API in server_render_js.

Strengths:

  • Switched from buildConsoleReplay() to getConsoleReplayScript()
  • Conditional wrapping respects replay_console option
  • Proper nil handling (postfix if leaves variable uninitialized if false, but interpolation handles it)

Lines 398-401, 423-426: Consistent ternary pattern ensures strings (not nil).

Strengths:

  • Uses ternary operator: replay_console ? wrap_console_script_with_nonce(console_script) : ""
  • Prevents "nil" string in output

Lines 440-464: The star of the show - excellent helper method.

Strengths:

  • Blank input guard (line 444)
  • Rails version compatibility with exception handling (449-456)
  • Correct use of content_tag for attribute escaping
  • Well-documented security assumption (441-442)
  • Proper use of html_safe given pre-sanitization

Note: Using rescue ArgumentError for control flow is slightly inelegant but pragmatic. Checking method arity would be cleaner but this approach is simpler and correct:

# Alternative (more complex, not necessary):
if method(:content_security_policy_nonce).arity == -1
  content_security_policy_nonce(:script)
else
  content_security_policy_nonce
end

Current approach is fine - exception handling overhead is negligible in SSR context.

packages/react-on-rails/tests/buildConsoleReplay.test.js

Lines 79-125: Comprehensive test suite.

Strengths:

  • Tests nonce inclusion (79-86)
  • Tests empty console edge case (88-93)
  • Tests unwrapped output (95-109)
  • Tests XSS prevention (111-125) - this is particularly valuable

The XSS test demonstrates that 'abc123" onload="alert(1)' becomes 'abc123onload=alert1', effectively neutralizing the attack.

CHANGELOG.md

Line 28: Perfect changelog entry.

Strengths:

  • User-facing description
  • Explains the "why" (CSP violations)
  • Notes cross-version compatibility
  • Links to PR and author
  • Proper formatting per CLAUDE.md guidelines

🔒 Comparison with CodeRabbit Reviews

I reviewed the extensive CodeRabbit feedback in the PR. Key points:

  1. XSS concerns in RenderUtils.ts: CodeRabbit initially flagged the nonce interpolation as unsafe. This was addressed by adding sanitization in commit 0046401. ✅ Resolved.

  2. Nil handling in helper.rb:246: CodeRabbit noted that postfix if could leave console_script_tag nil. This was fixed by using ternary operators in commits addressing the other rendering paths. ✅ Resolved.

  3. Ruby test coverage: CodeRabbit and subsequent reviews noted missing RSpec tests. UPDATE: Tests were added in spec/dummy/spec/helpers/react_on_rails_helper_spec.rb:755-872. ✅ Fully resolved.

All critical issues raised by automated reviews have been addressed. The implementation is solid.


🎓 What I Learned from This PR

This PR demonstrates several best practices worth highlighting:

  1. Defense-in-depth security: Multiple layers (JS sanitization + nonce sanitization + Rails escaping)
  2. Graceful degradation: Works with and without CSP configuration
  3. Cross-version compatibility: Handles API differences without version checks
  4. Test-driven security: XSS test explicitly verifies sanitization works
  5. Clear API boundaries: TypeScript returns data, Ruby handles framework integration

📋 Pre-Merge Checklist

Based on CLAUDE.md requirements:

  • RuboCop compliance: PR description states linting passed
  • Trailing newlines: No issues noted in reviews
  • CHANGELOG entry: Present and properly formatted
  • Test coverage: Both TypeScript and Ruby tests comprehensive
  • Documentation: JSDoc and inline comments present
  • Backward compatibility: Fully maintained
  • Security review: Multiple layers of protection verified

🚀 Recommendation

APPROVE AND MERGE

This PR is production-ready. All concerns raised in previous reviews have been addressed with comprehensive test coverage and proper security measures.

Optional Follow-Up (Post-Merge)

If time permits, consider:

  1. Enhancing the TypeScript comment to clarify base64url support (very minor)
  2. Adding an integration test that verifies the nonce works end-to-end with actual CSP headers (nice-to-have)

Summary

Excellent work on this security enhancement! The implementation demonstrates:

  • Strong security practices with defense-in-depth
  • Thoughtful API design with clear separation of concerns
  • Comprehensive test coverage (TypeScript and Ruby sides)
  • Perfect backward compatibility
  • Cross-version Rails support (5.2-7.x)

The CSP nonce support will prevent violations for users with restrictive policies, and the implementation is robust and well-tested. This is high-quality, production-ready code. 🎉


View job

@justin808 justin808 merged commit e2ca31a into master Nov 21, 2025
29 of 30 checks passed
@justin808 justin808 deleted the justin808/csp-nonce-console-replay branch November 21, 2025 04:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CSP nonce support for consoleReplay script

2 participants