Skip to content

Consolidate CSP nonce sanitization into shared module#2828

Merged
justin808 merged 9 commits intomainfrom
jg/2582-nonce-sanitize-dedup
Apr 12, 2026
Merged

Consolidate CSP nonce sanitization into shared module#2828
justin808 merged 9 commits intomainfrom
jg/2582-nonce-sanitize-dedup

Conversation

@justin808
Copy link
Copy Markdown
Member

@justin808 justin808 commented Mar 24, 2026

Summary

  • Extracted duplicate sanitizeNonce logic from OSS RenderUtils.ts and Pro utils.ts into a single react-on-rails/sanitizeNonce module
  • Pro package now imports from OSS instead of maintaining its own copy
  • Added comprehensive tests (11 cases) covering valid nonces, injection attempts, edge cases
  • Documented the sanitize-then-validate policy: malformed nonces are silently dropped (no nonce attribute emitted) rather than rejecting the render or logging sensitive values

Fixes #2582

Test plan

  • OSS sanitizeNonce.test.ts — 11 tests pass
  • OSS buildConsoleReplay.test.ts — still passes (uses wrapInScriptTags which now imports the shared module)
  • Pro injectRSCPayload.test.ts — nonce sanitization and valid nonce tests pass
  • Pro transformRSCStreamAndReplayConsoleLogs.test.ts — nonce tests pass
  • Full pnpm run build succeeds
  • pnpm run type-check passes
  • Prettier formatting verified

🤖 Generated with Claude Code


Note

Medium Risk
Touches CSP nonce handling used to generate/attach <script> nonces; mistakes could weaken CSP protections or break script execution. Scope is small and behavior is now unit-tested, reducing regression risk.

Overview
Centralizes CSP nonce sanitization by introducing packages/react-on-rails/src/sanitizeNonce.ts, wiring wrapInScriptTags to use it, and exporting it as react-on-rails/@internal/sanitizeNonce for reuse.

Removes the duplicated sanitizeNonce implementation from react-on-rails-pro/src/utils.ts and updates Pro RSC helpers (injectRSCPayload, transformRSCStreamAndReplayConsoleLogs) to import the shared sanitizer. Adds a new Jest suite covering nonce edge cases/injection attempts, and includes small maintenance updates to .lychee.toml exclusions and a formatting fix in PROJECTS.md.

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

Summary by CodeRabbit

  • Bug Fixes

    • Improved CSP nonce handling by centralizing sanitization and preserving previous validation/behavior.
  • Chores

    • Consolidated nonce validation into a shared export and adjusted streaming helpers for compatibility with newer async runtimes.
  • Tests

    • Added unit tests covering nonce sanitization, validation, and injection-edge cases.

justin808 and others added 2 commits March 23, 2026 20:49
The sanitizeNonce logic was duplicated in the OSS RenderUtils.ts and
Pro utils.ts. Extract it into a dedicated react-on-rails/sanitizeNonce
module so both packages share a single implementation. Add comprehensive
tests for the sanitize-then-validate policy. Malformed nonces are
silently dropped rather than rejected or logged.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 24, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Consolidates CSP nonce sanitization into a new OSS-exported sanitizeNonce function; Pro packages import and use this shared implementation, removing the duplicate Pro helper. Adds tests and a package subpath export; minor CLI typing change and changelog entry included.

Changes

Cohort / File(s) Summary
OSS: sanitizeNonce & tests
packages/react-on-rails/src/sanitizeNonce.ts, packages/react-on-rails/tests/sanitizeNonce.test.ts, packages/react-on-rails/package.json
Added sanitizeNonce default export (sanitize-then-validate base64/base64url with optional padding), tests, and package subpath export ./sanitizeNonce.
OSS: render utils
packages/react-on-rails/src/RenderUtils.ts
Replaced inline nonce sanitization logic with call to new sanitizeNonce; nonce attribute assembly unchanged.
Pro: import updates
packages/react-on-rails-pro/src/injectRSCPayload.ts, packages/react-on-rails-pro/src/transformRSCStreamAndReplayConsoleLogs.ts
Switched imports to default sanitizeNonce from react-on-rails/sanitizeNonce (removed local named import).
Pro: removed duplicate
packages/react-on-rails-pro/src/utils.ts
Removed exported local sanitizeNonce implementation.
Changelog & CLI
CHANGELOG.md, packages/create-react-on-rails-app/src/create-app.ts
Changelog entry added; createApp() refactored to explicit proModeLabel typing/assignment.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I found two nonces in the grass,

I hopped them into one clean pass.
A tidy function, tests that sing,
One little hop — secure everything! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the primary change: consolidating duplicate sanitizeNonce logic from multiple modules into a single shared module.
Linked Issues check ✅ Passed The PR successfully addresses both objectives from issue #2582: extracting duplicate nonce sanitization into shared react-on-rails/sanitizeNonce module and implementing the sanitize-then-validate policy across OSS and Pro.
Out of Scope Changes check ✅ Passed All changes directly support the consolidation objective; the minor TypeScript tidy-up in create-react-on-rails-app for pro/rsc labeling is incidental housekeeping compatible with the scope.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg/2582-nonce-sanitize-dedup

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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 24, 2026

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 62.63 KB (+0.01% 🔺)
react-on-rails/client bundled (gzip) (time) 62.63 KB (+0.01% 🔺)
react-on-rails/client bundled (brotli) 53.78 KB (+0.15% 🔺)
react-on-rails/client bundled (brotli) (time) 53.78 KB (+0.15% 🔺)
react-on-rails-pro/client bundled (gzip) 63.57 KB (+0.02% 🔺)
react-on-rails-pro/client bundled (gzip) (time) 63.57 KB (+0.02% 🔺)
react-on-rails-pro/client bundled (brotli) 54.55 KB (-0.09% 🔽)
react-on-rails-pro/client bundled (brotli) (time) 54.55 KB (-0.09% 🔽)
registerServerComponent/client bundled (gzip) 127.39 KB (+0.01% 🔺)
registerServerComponent/client bundled (gzip) (time) 127.39 KB (+0.01% 🔺)
registerServerComponent/client bundled (brotli) 61.59 KB (+0.11% 🔺)
registerServerComponent/client bundled (brotli) (time) 61.59 KB (+0.11% 🔺)
wrapServerComponentRenderer/client bundled (gzip) 122.02 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 122.02 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 56.73 KB (-0.06% 🔽)
wrapServerComponentRenderer/client bundled (brotli) (time) 56.73 KB (-0.06% 🔽)

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 24, 2026

Greptile Summary

This PR eliminates a duplicated CSP nonce sanitization implementation by extracting it into a single shared sanitizeNonce module (react-on-rails/sanitizeNonce) and updating both RenderUtils.ts (OSS) and two Pro-package consumers to import from it. The refactor is a clean extraction with no behavior change — the regex logic is byte-for-byte identical to both prior copies, and the new module is properly registered in package.json's exports map so the Pro package can resolve it.

  • New packages/react-on-rails/src/sanitizeNonce.ts module with clear JSDoc describing the sanitize-then-validate policy
  • 11-case test suite added covering valid nonces, padding variants, edge cases, and injection attempts
  • RenderUtils.ts inline sanitization replaced with a call to the shared module
  • Pro injectRSCPayload.ts and transformRSCStreamAndReplayConsoleLogs.ts updated to import from OSS instead of local utils.ts
  • ./sanitizeNonce export entry added to package.json
  • CHANGELOG.md entry uses a [PR PENDING] placeholder that should be updated to [PR 2828]

Confidence Score: 5/5

  • Safe to merge — pure extraction refactor with no behavior change and comprehensive test coverage.
  • The logic is identical to both prior implementations, the new export is correctly wired up in package.json, and 11 tests verify all edge cases. The only open item is a trivial CHANGELOG placeholder ([PR PENDING]) that can be fixed in this PR or immediately after merge.
  • No files require special attention.

Important Files Changed

Filename Overview
packages/react-on-rails/src/sanitizeNonce.ts New shared module extracted from inline logic in RenderUtils.ts and Pro utils.ts; implementation is identical to both prior copies, well-documented, and properly exported.
packages/react-on-rails/tests/sanitizeNonce.test.ts Comprehensive 11-case test suite covering undefined/empty input, valid base64/base64url nonces, padding variants, and injection attempts; all cases correctly document and verify the sanitize-then-validate behavior.
packages/react-on-rails/package.json Correctly registers the new ./sanitizeNonce export pointing to ./lib/sanitizeNonce.js, enabling the Pro package to import it via the package exports map.
packages/react-on-rails/src/RenderUtils.ts Inline nonce sanitization logic removed and replaced with a call to the shared module; behavior is unchanged.
packages/react-on-rails-pro/src/utils.ts Duplicate sanitizeNonce export removed; remaining exports are unaffected.
packages/react-on-rails-pro/src/injectRSCPayload.ts Import updated from local ./utils.ts to the shared OSS module; usage is unchanged.
packages/react-on-rails-pro/src/transformRSCStreamAndReplayConsoleLogs.ts Import updated from local ./utils.ts to the shared OSS module; usage is unchanged.
CHANGELOG.md New entry correctly describes the consolidation, but uses a [PR PENDING] placeholder instead of the actual PR number 2828.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["RenderUtils.ts (OSS)"]
    B["injectRSCPayload.ts (Pro)"]
    C["transformRSCStreamAndReplayConsoleLogs.ts (Pro)"]
    D["sanitizeNonce.ts\n(react-on-rails/sanitizeNonce)"]

    subgraph Before
        A1["RenderUtils.ts\n(inline regex logic)"]
        B1["injectRSCPayload.ts\nimports from utils.ts"]
        C1["transformRSCStream…\nimports from utils.ts"]
        U["utils.ts (Pro)\nexports sanitizeNonce"]
        B1 --> U
        C1 --> U
    end

    subgraph After
        A --> D
        B --> D
        C --> D
    end
Loading

Reviews (1): Last reviewed commit: "Update CHANGELOG with actual PR number (..." | Re-trigger Greptile

Comment thread CHANGELOG.md Outdated
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 24, 2026

Review: Consolidate CSP nonce sanitization into shared module

Overall this is a clean, well-structured refactor. The extraction eliminates real duplication, the test coverage is solid (11 cases covering the key paths), and the Pro package correctly imports from the shared OSS module. A few points worth addressing:

Docstring accuracy: The JSDoc says 'If the sanitized value does not match, undefined is returned and no nonce attribute will be emitted'. This overstates the drop guarantee. The function can return a sanitized-but-garbage value when stripped remnants of malformed input happen to match the validation regex. Input ><script>alert(1)</script> after stripping becomes scriptalert1/script, which passes validation and is returned as the nonce value. The behavior is injection-safe (all dangerous chars are stripped), but the docstring implies a stricter drop policy than the code enforces.

Misleading test case: The test 'strips dangerous characters from injection attempts' and its comment 'After stripping non-base64 chars, the remaining alphanumeric string is valid' reads as if returning a garbage nonce is expected/desirable. In practice scriptalert1/script will be emitted as a nonce attribute value — it won't match any real browser nonce so CSP blocks the script. That is fine security-wise, but a clearer comment noting the output is not meaningful as a real nonce would help future readers.

Public export surface: sanitizeNonce is now a named public export under ./sanitizeNonce. If end-users are expected to import it directly, document it in the public API. If it is only meant for Pro package internal use, consider using the @internal export prefix (similar to ./@internal/base/client) to avoid accidentally committing to this as a stable API.

Minor: The = character is included in the strip allowlist ([^a-zA-Z0-9+/=_-]) but is restricted to trailing position in the validate regex (^[a-zA-Z0-9+/_-]+={0,2}$). This is correct, but a short comment on the two-phase treatment of = would help future readers.

The core change is correct and the existing tests confirm security-critical paths work as intended. The above are documentation and API surface concerns rather than functional bugs.

Comment thread packages/react-on-rails/src/sanitizeNonce.ts
Comment thread packages/react-on-rails/tests/sanitizeNonce.test.ts Outdated
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 24, 2026

Overall: Clean, well-motivated refactor that eliminates the keep-in-sync comment by actually sharing code. Tests cover the core scenarios well.

One security observation worth calling out: the sanitize-then-validate approach means certain injection-looking strings can pass through rather than being rejected. The test at line 38 of sanitizeNonce.test.ts documents this edge case. After stripping non-base64 characters from a script-tag payload, the remaining string scriptalert1/script clears the validation regex and becomes the emitted nonce value. This is not a practical XSS risk since CSP nonces come from the server-generated Content-Security-Policy header (an attacker controlling the nonce value implies a much larger compromise), but it is surprising behavior that a security reviewer could flag.

Two small improvements worth considering: (1) The test name "strips dangerous characters from injection attempts" implies the function rejects the input, but it actually accepts the stripped result. A clearer name like "produces stripped nonce from base64-passable injection string" matches the assertion. (2) The module JSDoc says malformed nonces are dropped, which is only partially true. Nonces that produce a valid base64-like string after stripping are NOT dropped. One sentence acknowledging this edge case would prevent future confusion.

Minor nit: the test "returns undefined for a nonce that is only padding characters" passes "===". Since = is preserved by the strip regex but only allowed at the end of the validate regex, this test relies on the interaction of both steps. A brief inline comment explaining why === returns undefined (validate requires at least one non-padding char before any =) would help readers understand the two-step logic.

Everything else looks correct and consistent with the pre-existing regex. Changelog entry is accurate and the package.json exports entry follows the existing pattern.

Comment thread packages/react-on-rails/tests/sanitizeNonce.test.ts Outdated
Comment thread packages/react-on-rails/src/sanitizeNonce.ts Outdated
Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
CHANGELOG.md (1)

37-40: Please avoid changelog entries for refactors with unchanged behavior.

Line 39 describes internal consolidation and explicitly says behavior is unchanged. Per changelog policy, this should be omitted (or rewritten only if there is a user-visible behavior change).

Suggested edit
-#### Changed
-
-- **Consolidated CSP nonce sanitization**: Extracted the duplicate `sanitizeNonce` logic from `RenderUtils.ts` (OSS) and `utils.ts` (Pro) into a single shared `sanitizeNonce` module in the OSS package (`react-on-rails/sanitizeNonce`). The Pro package now imports from the OSS package instead of maintaining its own copy. Behavior is unchanged: malformed nonces are silently dropped (sanitize-then-validate policy). [PR 2828](https://github.com/shakacode/react_on_rails/pull/2828) by [justin808](https://github.com/justin808). Fixes [Issue 2582](https://github.com/shakacode/react_on_rails/issues/2582).

As per coding guidelines, update /CHANGELOG.md for user-visible changes only and exclude refactoring.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` around lines 37 - 40, The CHANGELOG entry about consolidating
`sanitizeNonce` is a refactor with no user-visible behavior change and should be
removed or rewritten; edit the `CHANGELOG.md` section containing the
"Consolidated CSP nonce sanitization" bullet so that you either delete this
bullet entirely or replace it with a note only if there is a user-facing change,
ensuring no internal-refactor-only text remains (refer to the exact
"Consolidated CSP nonce sanitization" bullet and its mention of `sanitizeNonce`,
`RenderUtils.ts`, and `utils.ts` to locate the text).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@CHANGELOG.md`:
- Around line 37-40: The CHANGELOG entry about consolidating `sanitizeNonce` is
a refactor with no user-visible behavior change and should be removed or
rewritten; edit the `CHANGELOG.md` section containing the "Consolidated CSP
nonce sanitization" bullet so that you either delete this bullet entirely or
replace it with a note only if there is a user-facing change, ensuring no
internal-refactor-only text remains (refer to the exact "Consolidated CSP nonce
sanitization" bullet and its mention of `sanitizeNonce`, `RenderUtils.ts`, and
`utils.ts` to locate the text).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 69f4ddca-280d-43da-aff1-31f956a2c50e

📥 Commits

Reviewing files that changed from the base of the PR and between c8267da and 4403c17.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • packages/react-on-rails-pro/src/injectRSCPayload.ts
  • packages/react-on-rails-pro/src/transformRSCStreamAndReplayConsoleLogs.ts
  • packages/react-on-rails-pro/src/utils.ts
  • packages/react-on-rails/package.json
  • packages/react-on-rails/src/RenderUtils.ts
  • packages/react-on-rails/src/sanitizeNonce.ts
  • packages/react-on-rails/tests/sanitizeNonce.test.ts
💤 Files with no reviewable changes (1)
  • packages/react-on-rails-pro/src/utils.ts

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copy link
Copy Markdown
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Around line 68-71: The new CHANGELOG entry adds a duplicate "#### Changed"
header and documents an internal refactor (consolidation of sanitizeNonce) as a
user-visible change; either remove the entry entirely or convert it to an
internal note per policy. Edit CHANGELOG.md to delete or relocate the passage
about extracting sanitizeNonce (references: sanitizeNonce, RenderUtils.ts,
utils.ts, react-on-rails/sanitizeNonce) so the changelog contains only
user-visible changes—if you keep a note, mark it clearly as an internal/refactor
note under an appropriate internal/dev-only section rather than a top-level
"Changed" heading.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8e397bb5-2bed-42a0-a2b5-250f8c3c7c16

📥 Commits

Reviewing files that changed from the base of the PR and between a73a2e6 and 54cb478.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • packages/react-on-rails/package.json
✅ Files skipped from review due to trivial changes (1)
  • packages/react-on-rails/package.json

Comment thread CHANGELOG.md Outdated
Comment thread packages/react-on-rails/package.json Outdated
Comment thread packages/react-on-rails/src/sanitizeNonce.ts
Comment thread packages/react-on-rails/tests/sanitizeNonce.test.ts
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 27, 2026

Overall this is a clean, well-scoped refactor. The regex is preserved exactly from both call sites so behaviour is genuinely unchanged, coverage is thorough, and the TypeScript narrowing in create-app.ts is a nice improvement. Three things worth addressing:

API surface (medium): sanitizeNonce is exported at the public subpath react-on-rails/sanitizeNonce, but Pro already uses react-on-rails/@internal/base/client and react-on-rails/@internal/base/full for cross-package utilities not part of the user-facing API. sanitizeNonce is the same kind of internal utility — using @internal/sanitizeNonce would be consistent with that convention and avoids locking in a public path for an implementation detail. See inline comment on package.json.

Missing edge-case test (minor): The strip regex allows = anywhere, but the validate regex only permits trailing = padding. A nonce like 'abc=def' will silently return undefined. A test for this would make the contract explicit and guard against future regex edits. See inline comment on sanitizeNonce.ts.

Partially-stripped injection result (documentation): The test confirming '><script>alert(1)</script>' returns 'scriptalert1/script' is correct and safe, but the comment should explain why it is safe: the returned value can never match a server-issued nonce, so CSP still blocks the script regardless. Without that context a future reader might flag it as a security hole. See inline comment on the test file.

…olve-2828

* origin/main:
  Bump version to 16.5.1
  Update CHANGELOG.md for 16.5.1 (#2873)
  fix: include lib/tasks/ in pro gem so rake tasks are available (#2872)
  docs: clarify how React on Rails compares to alternatives (#2856)
  docs: fix published setup guidance gaps (#2860)
  docs: refresh setup and runtime guidance (#2857)
  docs: refresh pro upgrade examples (#2859)
  docs: modernize dependency update commands (#2864)
  Update spec/dummy Gemfile.lock for async >= 2.29 (#2870)

# Conflicts:
#	CHANGELOG.md
Copy link
Copy Markdown
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.

♻️ Duplicate comments (1)
CHANGELOG.md (1)

64-68: ⚠️ Potential issue | 🟡 Minor

Remove the duplicated/invalid changelog block under 16.5.0.

This block repeats the PR 2832 entry, introduces a duplicate #### Changed heading, and adds a refactor-only note (“behavior is unchanged”), which should not be listed as a user-visible changelog item. Also, use [2828](...) style instead of [PR 2828](...) for any retained entry.

Suggested edit
-#### Changed
-
-- **[Pro]** **Migrated from `Async::Variable` to `Async::Promise`**: The streaming helper internals now use `Async::Promise` for async v2.29+ compatibility while preserving pre-first-chunk error propagation behavior. [PR 2832](https://github.com/shakacode/react_on_rails/pull/2832) by [justin808](https://github.com/justin808). Fixes [Issue 2563](https://github.com/shakacode/react_on_rails/issues/2563).
-
-- **Consolidated CSP nonce sanitization**: Extracted the duplicate `sanitizeNonce` logic from `RenderUtils.ts` (OSS) and `utils.ts` (Pro) into a single shared `sanitizeNonce` module in the OSS package (`react-on-rails/sanitizeNonce`). The Pro package now imports from the OSS package instead of maintaining its own copy. Behavior is unchanged: sanitize-then-validate is applied, and values that still fail nonce-pattern validation after sanitization are omitted. [PR 2828](https://github.com/shakacode/react_on_rails/pull/2828) by [justin808](https://github.com/justin808). Fixes [Issue 2582](https://github.com/shakacode/react_on_rails/issues/2582).

As per coding guidelines: “Update /CHANGELOG.md for user-visible changes only ... exclude ... refactoring ...” and “Format CHANGELOG entries as [PR NUMBER](url) by [username](url) without hash before PR number.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` around lines 64 - 68, Remove the duplicated/invalid changelog
block under version 16.5.0: delete the repeated PR 2832 entry and the duplicate
"#### Changed" heading, strip the refactor-only note ("behavior is unchanged")
so the changelog only contains user-visible changes, and for any retained
consolidation entry convert the PR link format from "[PR 2828](...)" to
"[2828](...)" and keep the wording concise (mentioning sanitizeNonce and that it
consolidated logic from RenderUtils.ts and utils.ts into the OSS package).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@CHANGELOG.md`:
- Around line 64-68: Remove the duplicated/invalid changelog block under version
16.5.0: delete the repeated PR 2832 entry and the duplicate "#### Changed"
heading, strip the refactor-only note ("behavior is unchanged") so the changelog
only contains user-visible changes, and for any retained consolidation entry
convert the PR link format from "[PR 2828](...)" to "[2828](...)" and keep the
wording concise (mentioning sanitizeNonce and that it consolidated logic from
RenderUtils.ts and utils.ts into the OSS package).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2e2dbe67-efaf-4823-8932-8c3066d7de11

📥 Commits

Reviewing files that changed from the base of the PR and between 54cb478 and e47e33a.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • packages/react-on-rails/package.json
✅ Files skipped from review due to trivial changes (1)
  • packages/react-on-rails/package.json

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 28, 2026

Code Review — PR #2828: Consolidate CSP nonce sanitization

Overall this is a clean, well-structured refactor. The new sanitizeNonce module is correct, the tests are comprehensive, and the Pro package imports are updated properly. Three issues need attention before merge.

🔴 CHANGELOG structural bug — duplicate #### Changed section + duplicated entry

The PR adds a second #### Changed block inside the existing [16.5.0] release, and it also duplicates the Async::VariableAsync::Promise entry that already exists earlier in that same release section (under the first #### Changed). This creates two #### Changed subsections in one release and repeats an entry from a different PR (2832).

The fix is to:

  1. Remove the new #### Changed header that this PR adds (there is already one in [16.5.0]).
  2. Remove the duplicate Async::Variable bullet from this PR's additions.
  3. Append only the nonce sanitization bullet to the existing #### Changed block.

🟡 CHANGELOG policy — refactor-only entry

Per the project's CHANGELOG guidelines: "Update CHANGELOG.md for user-visible changes only. Do NOT add entries for: linting, formatting, refactoring, tests, or documentation fixes."

The nonce sanitization entry explicitly states "Behavior is unchanged", which flags it as a refactor note. However, the PR also adds ./sanitizeNonce as a named public export in package.json — that is a user-visible API addition. Consider either:

  • Removing the entry entirely (if the intent is purely internal), and removing the public ./sanitizeNonce export (keeping the module internal), or
  • Rewriting the changelog entry to highlight the new public react-on-rails/sanitizeNonce export as an Added item, without the "behavior is unchanged" framing.

🟢 sanitizeNonce.ts — regex character class clarity

In the validation regex /^[a-zA-Z0-9+/_-]+={0,2}$/, the - at the end of the character class is parsed as a literal (correct), but the sequence _- reads like a range from _ (95) to - (45), which is an invalid range and could cause confusion during code review. Moving - to the very start of the class ([-a-zA-Z0-9+/_]) or escaping it ([a-zA-Z0-9+/\_\-]) makes the intent unambiguous. (This is a clarity nit; the current code is functionally correct in JavaScript because a trailing - is always literal.)

Comment thread CHANGELOG.md Outdated
Comment thread packages/react-on-rails/src/sanitizeNonce.ts
Copy link
Copy Markdown
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Around line 64-66: Remove the duplicated "#### Changed" heading and the
repeated changelog entry that starts with "**[Pro]** **Migrated from
`Async::Variable` to `Async::Promise`**: The streaming helper internals..." (the
duplicate that repeats the PR 2832 and Issue 2563 references); keep the original
single entry earlier in the file and delete this second occurrence so the MD024
duplicate-heading rule and the duplicate entry are eliminated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c8879292-32fb-4fe0-91f9-36a2810de3c2

📥 Commits

Reviewing files that changed from the base of the PR and between e47e33a and 0ba5bed.

📒 Files selected for processing (1)
  • CHANGELOG.md

Comment thread CHANGELOG.md Outdated
Comment thread CHANGELOG.md Outdated
Comment thread packages/react-on-rails/src/sanitizeNonce.ts
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 28, 2026

Review: Consolidate CSP nonce sanitization into shared module

Overall: Clean refactor, good test coverage. One CHANGELOG issue and one behavioural question worth addressing before merge.

Summary of changes

The extraction of duplicate nonce sanitization logic into react-on-rails/sanitizeNonce is well-motivated and the implementation is correct. The Pro package correctly imports from the OSS module, and the new export is wired up in package.json. TypeScript type tightening in create-app.ts is a nice bonus.


Issues found

1. CHANGELOG duplicate / wrong PR — needs fix

The #### Changed block added at the bottom of the [16.5.0] section is identical to the entry already present at line 46 and references PR 2832 (the Async::Variable → Async::Promise migration), not this PR. Looks like it was accidentally carried over when branching from that work. See inline comment on CHANGELOG.md.

2. Sanitize-then-validate: mangled injection inputs can still emit a nonce

'><script>alert(1)</script>' strips to 'scriptalert1/script' → passes validation → returned as the nonce value. This is attribute-injection safe (all HTML metacharacters are stripped), but it means a clearly corrupted nonce is emitted rather than silently dropped. See inline comment on sanitizeNonce.ts for a validate-only alternative. If the current behaviour is intentional, the doc comment should make that explicit.


Minor observations (no action required)

  • No minimum-length guard: A single-character string like 'a' passes through. CSP nonces should be ≥128 bits; the function accepts anything matching the base64 alphabet. Fine for an injection-safety sanitizer, but worth noting if this function is ever reused as a validator.
  • Test coverage is solid — 11 cases covering injection attempts, padding variants, and the full base64url alphabet.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 28, 2026

Code Review: Clean refactoring of CSP nonce sanitization. Behavior preserved exactly from both source files. Three observations: (1) The sanitize-then-validate approach can emit a stripped but non-matching nonce (e.g. scriptalert1/script from injection input) which is attribute-safe but still causes CSP nonce mismatch - script is blocked, but the nonce attr in HTML may confuse debugging. (2) No test covers equals-sign in middle position (abc=def) - strip keeps it, validation rejects it, returns undefined, which is correct but undocumented. (3) Minor: regex accepts mixed base64 and base64url chars simultaneously (harmless superset). Overall: solid work - well-documented, tests thorough, no behavioral change.

Comment thread packages/react-on-rails/src/sanitizeNonce.ts
Comment thread packages/react-on-rails/src/sanitizeNonce.ts
Comment thread packages/react-on-rails/tests/sanitizeNonce.test.ts
Comment thread packages/react-on-rails/src/sanitizeNonce.ts
Comment thread packages/react-on-rails/tests/sanitizeNonce.test.ts
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 28, 2026

Review

Overall: clean consolidation, good test coverage — a few security-policy questions worth settling before merge.

What's good

  • The extraction removes real duplication and the "NOTE: keep this in sync" comment is gone, which is always a win.
  • The new export path (react-on-rails/sanitizeNonce) is correctly added to package.json exports.
  • 11 test cases is solid for a function this small.
  • create-app.ts simplification is correct and the narrower literal-union type ('--rsc' | '--pro' | null) is a genuine improvement.

Concerns

1. Sanitize-then-validate policy leaves a silent-accept path uncovered by tests

The strip regex and validate regex are intentionally asymmetric: stripping is permissive (keeps = anywhere), but validation requires = only at the end. As a side effect, every injection test in the suite happens to fail for a structural reason (e.g. = in onload=alert ends up mid-string, breaking the pattern). There is currently no test exercising the other branch of the policy: a nonce that is stripped and still passes validation.

Example: sanitizeNonce('abc123"')'abc123'. The trailing " is silently dropped and the stripped value is used. This is documented, but it means a server bug that appends an extra character to the nonce value would be silently "fixed" rather than surfaced. A test explicitly covering this case would make the intent unambiguous.

2. The XSS-payload test produces a non-empty output

sanitizeNonce('"><script>alert(1)</script>') returns 'scriptalert1/script' — attribute-safe, and will never match a real CSP nonce, so no security issue in practice. But it means the function emits a nonce attribute for a value that was constructed entirely from an XSS payload. An alternative design — reject if any character was stripped — would return undefined for all tainted inputs and be simpler to reason about. The current design trades that predictability for not breaking renders when a nonce has minor encoding noise. Whether that trade-off is right for this codebase is worth an explicit comment in the source.

3. Hybrid base64 / base64url acceptance

Both +/ (standard base64) and -_ (base64url) are allowed simultaneously. A real CSP nonce uses one alphabet. This is harmless but the permissiveness is implicit. See inline comment for details.


Minor: the new ./sanitizeNonce entry in package.json exports is inserted between ./buildConsoleReplay and ./ReactDOMServer — no alphabetical ordering issue per se, but if the project has a convention for export ordering it's worth checking.

…e-dedup

* origin/main: (51 commits)
  Fix Content-Length mismatch and null renderingRequest errors in node renderer (#3069)
  Improve memory debugging docs with simpler heap snapshot approach (#3072)
  Enforce strict version matching in doctor and recommend doctor on errors (#3070)
  Remove immediate_hydration feature from codebase (#2834)
  Fix infinite fork loop when node renderer worker fails to bind port (#2881)
  Fix TanStack Router SSR hydration mismatch in Pro async path (#2932)
  Improve node renderer error messages for malformed render requests (#3068)
  Add interactive mode prompt to create-react-on-rails-app (#3063)
  docs: replace hardcoded version numbers with unversioned install commands (#2893)
  Remove Contributing section from docs sidebar (#3064)
  Consolidate docs comparison pages into single evaluation entry (#3065)
  Docs route cleanup: canonicalize worst verbose URL slugs (#3067)
  Add agent summary blocks to high-value docs pages (#3066)
  Fix brittle positional assertions in create-app tests (#2923)
  Auto-resolve renderer password from ENV in Rails Pro (#2921)
  Standardize bundle env vars from =yes to =true (#2925)
  Fix Pro generator multiline and template-literal rewrites (#2918)
  Fix spec/dummy Procfile.dev SERVER_BUNDLE_ONLY to match template convention (#2922)
  Add llms docs entry points (#2916)
  Bump version to 16.6.0.rc.0
  ...

# Conflicts:
#	packages/create-react-on-rails-app/src/create-app.ts
Comment thread packages/react-on-rails/src/sanitizeNonce.ts
Comment thread packages/react-on-rails/tests/sanitizeNonce.test.ts
Comment thread packages/react-on-rails/package.json
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 8, 2026

Review: Consolidate CSP nonce sanitization into shared module

Overall: Good consolidation of duplicate logic — the implementation is functionally identical to what it replaces, the test coverage is a clear improvement, and the @internal naming on the export signals intent appropriately. A few issues worth addressing before merge.


Security concern: sanitize-then-validate vs. validate-then-reject

The core function (inline comment on sanitizeNonce.ts:16) strips invalid characters from the input, then validates the result. This means:

  • 'abc 123''abc123' (silently returns a different nonce than the server issued)
  • '"><script>alert(1)</script>''scriptalert1/script' (adversarial input partially passes through)

The mutation path is attribute-injection-safe (no " or > can survive), but it creates subtle failure modes: if the server somehow emits a nonce with extraneous characters (encoding artifact, middleware bug), the sanitized value won't match the CSP header and the script will be silently blocked, with the nonce appearing valid in the HTML. A validate-then-reject policy (reject the entire nonce if any invalid character is present) would be stricter and easier to reason about — see the inline comment for a concrete alternative.


Behavior parity confirmed ✓

The new sanitizeNonce.ts is byte-for-byte equivalent to the removed logic in both RenderUtils.ts and react-on-rails-pro/src/utils.ts. No behavior change introduced.


Minor: @internal export is publicly accessible

The ./@internal/sanitizeNonce export in package.json is not enforced by Node's package system — any consumer can import it. Worth a changelog note flagging it as an unstable internal API with no SemVer guarantees.


Nit: test description

The test case at sanitizeNonce.test.ts:36 ('strips invalid chars from injection input and returns the remaining nonce text') describes the mechanism but not the actual output. A description like 'returns a stripped nonce derived from XSS input (attribute-safe)' would make the behavior immediately obvious from the test name without reading the body.

@justin808
Copy link
Copy Markdown
Member Author

Address-review checkpoint posted at 2026-04-09T08:02:52Z.

Scan window: full PR history (2026-03-24T06:50:15Z2026-04-08T07:37:26Z) because no previous <!-- address-review-summary --> comment existed.
Cutoff mode: default workflow cutoff (no check all reviews override used).

Mattered

  • Confirmed and closed addressed blocking items from earlier review passes:
    • ./sanitizeNonce export concern was handled by using ./@internal/sanitizeNonce.
    • abc=def middle-padding behavior now has explicit test coverage.
    • CSP-mismatch implication is now called out in sanitizer test comments.
    • Duplicate CHANGELOG.md section/commentary was removed.
  • Policy-change suggestions (sanitize-then-validate vs validate-then-reject) were reviewed and intentionally deferred: this PR remains behavior-preserving consolidation.

Skipped

  • Regex character-class formatting nit (- placement) with no behavioral impact.
  • Test-name wording preferences where test body/comments already capture behavior.
  • General notes about @internal discoverability/changelog annotation for internal-only surfaces.
  • Duplicate/status-only bot summary comments that did not add new actionable deltas.

Follow-up issue: none created (no deferred must-fix items).

Future full-PR scans should start after this comment unless check all reviews is explicitly requested.

@justin808 justin808 merged commit 6e154b2 into main Apr 12, 2026
69 checks passed
@justin808 justin808 deleted the jg/2582-nonce-sanitize-dedup branch April 12, 2026 21:53
justin808 added a commit that referenced this pull request Apr 12, 2026
…ages

* origin/main: (44 commits)
  Consolidate CSP nonce sanitization into shared module (#2828)
  Add comprehensive --rsc-pro generator tests (#3098)
  fix: cross-env validation and docs for renderer password (#3090)
  Improve package metadata and Pro upgrade CTAs (#3112)
  docs: standardize warning syntax to GFM alert format (#3115)
  docs: improve react-intl documentation for React Server Components (#3085)
  Fix generator CI SSR regression on main (#3110)
  Refocus GitHub README on docs navigation (#3113)
  Add manual dev environment testing checklist for coding agents (#3074)
  Bump version to 16.6.0
  Update CHANGELOG.md for 16.6.0 (#3078)
  fix: node-renderer diagnostic improvements (#3086)
  fix: pin third-party npm deps in generator to prevent peer dep conflicts (#3083)
  chore(deps): bump lodash from 4.17.23 to 4.18.1 in the npm-security group across 1 directory (#2920)
  fix: refactor formatExceptionMessage to accept generic request context (#2877)
  Bump version to 16.6.0.rc.1
  Update CHANGELOG.md for 16.6.0.rc.1 (#3079)
  Update CHANGELOG.md unreleased section (#3077)
  Fix Content-Length mismatch and null renderingRequest errors in node renderer (#3069)
  Improve memory debugging docs with simpler heap snapshot approach (#3072)
  ...

# Conflicts:
#	docs/pro/home-pro.md
#	docs/pro/react-on-rails-pro.md
#	docs/sidebars.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Follow-up: CSP nonce sanitization consolidation and validation policy

2 participants