Skip to content

fix(connection): block reserved internal event types from server messages#3340

Open
achowdhry-ripple wants to merge 2 commits into
XRPLF:mainfrom
achowdhry-ripple:fix/dge-6740-validate-event-type
Open

fix(connection): block reserved internal event types from server messages#3340
achowdhry-ripple wants to merge 2 commits into
XRPLF:mainfrom
achowdhry-ripple:fix/dge-6740-validate-event-type

Conversation

@achowdhry-ripple
Copy link
Copy Markdown
Contributor

@achowdhry-ripple achowdhry-ripple commented May 18, 2026

Summary

Closes #3318

Connection.onMessage previously called this.emit(data.type as string, data) with zero validation of data.type. A malicious or compromised rippled server could therefore send {"type":"connected"}, {"type":"disconnected"}, {"type":"error"}, or {"type":"reconnect"} and trigger Connection's internal state-event listeners (registered in Client), spoofing the local connection state, faking errors, or driving reconnect logic.

This PR introduces a denylist of reserved internal event names and refuses to forward server messages whose type collides with any of them. Such messages are dropped and surfaced via the existing ('error', 'badMessage', ...) channel so they remain observable.

A denylist (rather than the whitelist suggested in the issue) was chosen deliberately to preserve the existing "unrecognized message type" forward-compatibility contract in packages/xrpl/test/connection.test.ts. Unknown future rippled stream types continue to be forwarded as before; only reserved local event names are blocked.

Changes

  • packages/xrpl/src/client/connection.ts: add RESERVED_INTERNAL_EVENTS set and guard the emit in onMessage against it.
  • packages/xrpl/test/connection.test.ts: add unit tests covering all four reserved event names, including the subtle type: "error" case (verifies the raw payload object never reaches error listeners).
  • packages/xrpl/HISTORY.md: changelog entry under Unreleased -> Fixed.

Test plan

  • cd packages/xrpl && npm test -- connection.test.ts - new "drops server message with reserved internal event type" cases pass.
  • Existing "unrecognized message type" test still passes (forward-compat preserved).
  • Existing "emit stream messages" and "propagates error message" tests still pass (legitimate stream/error paths unaffected).
  • npm run lint in packages/xrpl clean.

…ages (DGE-6740)

A malicious or compromised rippled server could previously send
`{"type":"connected"}`, `{"type":"disconnected"}`, `{"type":"error"}`,
or `{"type":"reconnect"}` and have `Connection.onMessage` forward those
events to internal Connection/Client listeners — spoofing connection
state, faking errors, or triggering reconnect logic.

`onMessage` now refuses to forward server-supplied `data.type` values
that collide with these reserved internal event names. Such messages
are dropped and surfaced as a `badMessage` error. Unknown server event
types (e.g. future rippled stream types) continue to be forwarded so
forward-compatibility is preserved.

Fixes XRPLF#3318
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c6060ce5-850a-45ed-85eb-9e22e0b92290

📥 Commits

Reviewing files that changed from the base of the PR and between 383a45a and 6b41279.

📒 Files selected for processing (2)
  • packages/xrpl/src/client/connection.ts
  • packages/xrpl/test/connection.test.ts

Walkthrough

This PR prevents server-supplied messages whose data.type matches Connection internal events from being re-emitted: such messages are dropped and an error with code badMessage is emitted instead.

Changes

Server-Supplied Event Injection Prevention

Layer / File(s) Summary
Reserved events constant and message filtering
packages/xrpl/src/client/connection.ts
Adds RESERVED_INTERNAL_EVENTS and updates onMessage to check data.type; reserved types cause an error emission with code badMessage and the server event is not forwarded.
Tests verifying reserved-type rejection
packages/xrpl/test/connection.test.ts
Parameterized test sends reserved type values and asserts internal listeners are not invoked and a badMessage error is emitted; includes explicit assertion preventing spoofed type: "error" payload from being forwarded.
Changelog entry
packages/xrpl/HISTORY.md
Unreleased HISTORY.md bullet documenting refusal to forward server-supplied message types that collide with Connection internal event names.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested reviewers

  • pdp2121
  • justinr1234

Poem

A rabbit guards the garden gate,
With lists of names reserved, first-rate.
When servers send their spoofed disguise,
The filter stops them—no surprise! 🐰
Bad messages bounce; true events arise.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: blocking reserved internal event types from server messages in the connection module.
Description check ✅ Passed The PR description comprehensively covers the issue, solution, and implementation details across all required template sections.
Linked Issues check ✅ Passed The PR fully addresses issue #3318 by implementing a denylist of reserved internal event names to prevent server-controlled event injection.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the security fix: updating connection.ts with the denylist, adding comprehensive tests, and updating HISTORY.md.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/xrpl/src/client/connection.ts

Parsing error: The keyword 'import' is reserved

packages/xrpl/test/connection.test.ts

Parsing error: The keyword 'import' is reserved


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.

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 (2)
packages/xrpl/src/client/connection.ts (2)

370-386: ⚡ Quick win

Add type validation for data.type before checking reserved events.

The current code uses a type assertion (data.type as string) without runtime validation. While JSON-parsed messages will typically have string types, a malicious or malformed message could have data.type as a number, boolean, array, or object. For example, {"type": 123} would bypass the reserved-event check and emit event "123". Although non-string types won't collide with reserved event names (so this isn't a security bypass), adding explicit string validation improves robustness and makes the code's assumptions explicit.

Suggested validation
   if (data.type) {
-    const type = data.type as string
+    if (typeof data.type !== 'string') {
+      this.emit('error', 'badMessage', 'data.type must be a string', message)
+      return
+    }
+    const type = data.type
     // Refuse to forward server-supplied event names that collide with
     // Connection's internal state events. Otherwise a rogue server could
     // spoof local connection state by sending e.g. `{"type":"connected"}`
     // or `{"type":"error"}`. See DGE-6740.
     if (RESERVED_INTERNAL_EVENTS.has(type)) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/xrpl/src/client/connection.ts` around lines 370 - 386, The code
assumes data.type is a string by asserting `data.type as string`; instead
perform a runtime check on `data.type` before using it: verify typeof data.type
=== 'string' and only then assign to a local `type` variable and run the
`RESERVED_INTERNAL_EVENTS.has(type)` check and `this.emit(type, data)`; if
`data.type` is missing or not a string, call `this.emit('error', 'badMessage',
'Invalid or non-string message type; dropping.', message)` (or similar error
handling used elsewhere) and return to avoid emitting non-string event names.

33-37: ⚡ Quick win

Consider adding 'reconnecting' to the reserved events list.

Line 488 shows that 'reconnecting' is also emitted internally during automatic reconnection attempts. While less critical than the four reserved events (which directly affect connection state), a malicious server could send {"type":"reconnecting"} to spoof automatic reconnection behavior and confuse monitoring code. For consistency and defense in depth, consider adding it to the reserved set.

Suggested addition
 const RESERVED_INTERNAL_EVENTS: ReadonlySet<string> = new Set([
   'connected',
   'disconnected',
   'error',
   'reconnect',
+  'reconnecting',
 ])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/xrpl/src/client/connection.ts` around lines 33 - 37, The
RESERVED_INTERNAL_EVENTS set (RESERVED_INTERNAL_EVENTS) currently protects
internal event names but omits the internally-emitted 'reconnecting' event; add
the string 'reconnecting' to that ReadonlySet so external messages cannot spoof
the client's internal reconnection signal (ensure the literal 'reconnecting' is
added alongside 'connected','disconnected','error','reconnect' in the
RESERVED_INTERNAL_EVENTS declaration).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/xrpl/src/client/connection.ts`:
- Around line 370-386: The code assumes data.type is a string by asserting
`data.type as string`; instead perform a runtime check on `data.type` before
using it: verify typeof data.type === 'string' and only then assign to a local
`type` variable and run the `RESERVED_INTERNAL_EVENTS.has(type)` check and
`this.emit(type, data)`; if `data.type` is missing or not a string, call
`this.emit('error', 'badMessage', 'Invalid or non-string message type;
dropping.', message)` (or similar error handling used elsewhere) and return to
avoid emitting non-string event names.
- Around line 33-37: The RESERVED_INTERNAL_EVENTS set (RESERVED_INTERNAL_EVENTS)
currently protects internal event names but omits the internally-emitted
'reconnecting' event; add the string 'reconnecting' to that ReadonlySet so
external messages cannot spoof the client's internal reconnection signal (ensure
the literal 'reconnecting' is added alongside
'connected','disconnected','error','reconnect' in the RESERVED_INTERNAL_EVENTS
declaration).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 98c777fd-3f48-4e0c-a3c6-2d1ecb312d4e

📥 Commits

Reviewing files that changed from the base of the PR and between d739d47 and 383a45a.

📒 Files selected for processing (3)
  • packages/xrpl/HISTORY.md
  • packages/xrpl/src/client/connection.ts
  • packages/xrpl/test/connection.test.ts

Comment thread packages/xrpl/src/client/connection.ts
Comment thread packages/xrpl/src/client/connection.ts Outdated
Comment thread packages/xrpl/test/connection.test.ts Outdated
Comment thread packages/xrpl/test/connection.test.ts Outdated
Comment thread packages/xrpl/test/connection.test.ts Outdated
@@ -903,6 +903,97 @@ describe('Connection', function () {
TIMEOUT,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

browser tests are failing

- Include 'reconnecting' in the reserved internal-event denylist, since
  Connection also emits it during automatic reconnect attempts.
- Drop internal ticket reference from inline comments.
- Collapse the separate type:"error" test into the parameterized
  it.each so all reserved types (including 'error' and 'reconnecting')
  share one test body. The 'error' iteration retains the extra check
  that the raw spoofed payload never reaches 'error' listeners.
Comment on lines 371 to +373
if (data.type) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Should be true
this.emit(data.type as string, data)
const type = data.type as string
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 Severity: HIGH

No runtime typeof check on data.type. Since JSON.parse can produce arrays, a malicious server sending {"type":["connected"]} bypasses Set.has() (strict equality, array ≠ string) but eventemitter3's property-based lookup implicitly calls .toString(), resolving ["connected"] to "connected" — triggering the reserved internal event and fully defeating this denylist guard.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: Add a runtime typeof check to ensure data.type is actually a string before using it. Replace if (data.type) with if (typeof data.type === 'string'). This prevents non-string values (e.g., arrays) from bypassing the Set.has() denylist check, since typeof will return 'object' for arrays and the block will be skipped entirely.

⚠️ Experimental Feature: This code suggestion is automatically generated. Please review carefully.

Suggested change
if (data.type) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Should be true
this.emit(data.type as string, data)
const type = data.type as string
if (typeof data.type === 'string') {
const type = data.type

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@achowdhry-ripple This sounds like a legit comment

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.

Server-controlled event injection via unvalidated data.type

2 participants