[GH-ISSUE #2555] fix(browser/cdp-connection): session replies without identity are accepted #871

Closed
opened 2026-10-15 18:29:11 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 13, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2555

Summary

When REA sends a browser command within an attached target session, it accepts a correlated result that omits the session ID and continues using the connection. The documented transport contract requires the reply's session identity to match the selected session and requires malformed replies to fail the connection. The local reproduction demonstrates this missing validation for a malformed peer response; it does not establish a failure from a real browser or acceptance of another target's data.

Steps to reproduce

On the source baseline below with its locked dependencies installed, place this fixture in src/browser/replySessionIdentity.test.ts and run npm run test:focused -- src/browser/replySessionIdentity.test.ts. The fixture uses the existing loopback HTTP/WebSocket browser fixture. It obtains the target session from attachment, holds the session-scoped Page.enable command, sends a reply with the correct command ID and no session ID, and checks whether the command and a subsequent command resolve.

import { expect, it, vi } from "vitest";
import { CdpConnection } from "./CdpConnection.js";
import { startFakeCdpBrowser } from "../../tests/fixtures/fakeCdpBrowser.js";

it("observes acceptance of a reply with an omitted session ID", async () => {
  const browser = await startFakeCdpBrowser({ hangOnMethod: "Page.enable" });
  const connection = await CdpConnection.connect(
    browser.browserWebSocketUrl,
    "observe_web_session",
  );
  const signal = AbortSignal.timeout(5_000);
  try {
    const attached = await connection.send(
      "Target.attachToTarget",
      { targetId: "page-1", flatten: true },
      undefined,
      signal,
    );
    expect(attached).toEqual({ sessionId: "session-1" });
    const pending = connection.send("Page.enable", {}, "session-1", signal);
    await vi.waitFor(() => expect(browser.commands).toHaveLength(2));
    const command = browser.commands[1]!;
    expect(command.sessionId).toBe("session-1");
    browser.emitRawMessage(JSON.stringify({ id: command.id, result: {} }));
    await expect(pending).resolves.toEqual({});
    await expect(
      connection.send("Runtime.enable", {}, undefined, signal),
    ).resolves.toEqual({});
  } finally {
    await connection.close();
    await browser.close();
  }
});

The assertions describe the observed failure on the baseline. For controls on fresh connections, add sessionId: "session-1" to the reply and observe acceptance; use sessionId: "foreign-session" and observe protocol_error for both commands; or send Page.enable without a session and observe acceptance of the reply without a session ID.

Expected behavior

A reply to a command sent with an explicit target session must identify that same session. A correlated reply that omits this identity must reject the pending command with protocol_error and cause later commands on that connection to reject with protocol_error. Replies without a session ID remain valid for commands that did not select a session.

Actual behavior

In the local real-WebSocket reproduction, attachment returned session-1 and Page.enable carried that session. The peer's reply { "id": 2, "result": {} } resolved the pending command with {}. The following unscoped Runtime.enable command also resolved with {}, so the malformed response did not fail the connection. Matching-session replies also resolved, while foreign-session replies rejected both commands with protocol_error. An unscoped command accepted a reply without a session ID. Each of these four cases ran once on a fresh connection.

Evidence

  • Expected source: docs/browser-observation.md:200-208 requires malformed pending replies to poison the connection and correlated reply session IDs to match the selected session. src/browser/CdpTargetSession.ts:33-39 obtains a flat session, and src/browser/CdpObservationSession.ts:69-74 sends Page.enable within that session.
  • Failure source: The independent loopback real-WebSocket probe observed the omitted-ID reply and later command resolving. In src/browser/CdpConnection.ts:240-260, the correlated reply handler compares session IDs only when the reply contains that property, then accepts an object result.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

The focused verifier command exited 0 with four tests passing in 592 ms. Its assertions checked the current outcomes described above, rather than asserting that the documented rejection already works. The observation for the missing-ID case was:

{"kind":"omitted","reply":{"id":2,"result":{}},"outcome":{"status":"resolved","result":{}},"next":{"status":"resolved","result":{}}}

The supported browser-observation entry point opens an authorized target session and passes its session ID to the observation commands. The probe exercises that transport boundary directly. It does not run the public CLI/MCP workflow or a real Chrome-family browser. Numeric command correlation still works, and the experiment establishes no security-boundary violation.

Restoration check

For the same attached session and the same correlated reply without sessionId, observe protocol_error from the pending command and the next command on that connection. Preserve the control outcomes: matching-session replies succeed, foreign-session replies fail the connection, and commands without a selected session accept replies without a session ID. These results restore the documented transport contract without requiring a particular implementation.

Runtime or environment

Failure verified at a2d8b94dd5 and still present at 8b181716ac; Node v26.10.0; npm 11.19.1; Vitest 5.0.3; locked dependencies; loopback HTTP/WebSocket fake browser; one focused source-test file.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A malformed correlated response is accepted as a successful session-scoped command result, and later commands continue despite the documented fatal wire failure. This is a bounded behavioral defect for peers that omit the reply session ID. No real-browser incident, wrong-target result, or population frequency was established.
  • Workaround status: unverified
  • Workaround basis: A matching-session reply succeeds in the local control, but no real-browser run or practical user workaround for an affected peer was verified.
Originally created by @coygeek on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2555 ## Summary When REA sends a browser command within an attached target session, it accepts a correlated result that omits the session ID and continues using the connection. The documented transport contract requires the reply's session identity to match the selected session and requires malformed replies to fail the connection. The local reproduction demonstrates this missing validation for a malformed peer response; it does not establish a failure from a real browser or acceptance of another target's data. ## Steps to reproduce On the source baseline below with its locked dependencies installed, place this fixture in `src/browser/replySessionIdentity.test.ts` and run `npm run test:focused -- src/browser/replySessionIdentity.test.ts`. The fixture uses the existing loopback HTTP/WebSocket browser fixture. It obtains the target session from attachment, holds the session-scoped `Page.enable` command, sends a reply with the correct command ID and no session ID, and checks whether the command and a subsequent command resolve. ```ts import { expect, it, vi } from "vitest"; import { CdpConnection } from "./CdpConnection.js"; import { startFakeCdpBrowser } from "../../tests/fixtures/fakeCdpBrowser.js"; it("observes acceptance of a reply with an omitted session ID", async () => { const browser = await startFakeCdpBrowser({ hangOnMethod: "Page.enable" }); const connection = await CdpConnection.connect( browser.browserWebSocketUrl, "observe_web_session", ); const signal = AbortSignal.timeout(5_000); try { const attached = await connection.send( "Target.attachToTarget", { targetId: "page-1", flatten: true }, undefined, signal, ); expect(attached).toEqual({ sessionId: "session-1" }); const pending = connection.send("Page.enable", {}, "session-1", signal); await vi.waitFor(() => expect(browser.commands).toHaveLength(2)); const command = browser.commands[1]!; expect(command.sessionId).toBe("session-1"); browser.emitRawMessage(JSON.stringify({ id: command.id, result: {} })); await expect(pending).resolves.toEqual({}); await expect( connection.send("Runtime.enable", {}, undefined, signal), ).resolves.toEqual({}); } finally { await connection.close(); await browser.close(); } }); ``` The assertions describe the observed failure on the baseline. For controls on fresh connections, add `sessionId: "session-1"` to the reply and observe acceptance; use `sessionId: "foreign-session"` and observe `protocol_error` for both commands; or send `Page.enable` without a session and observe acceptance of the reply without a session ID. ## Expected behavior A reply to a command sent with an explicit target session must identify that same session. A correlated reply that omits this identity must reject the pending command with `protocol_error` and cause later commands on that connection to reject with `protocol_error`. Replies without a session ID remain valid for commands that did not select a session. ## Actual behavior In the local real-WebSocket reproduction, attachment returned `session-1` and `Page.enable` carried that session. The peer's reply `{ "id": 2, "result": {} }` resolved the pending command with `{}`. The following unscoped `Runtime.enable` command also resolved with `{}`, so the malformed response did not fail the connection. Matching-session replies also resolved, while foreign-session replies rejected both commands with `protocol_error`. An unscoped command accepted a reply without a session ID. Each of these four cases ran once on a fresh connection. ## Evidence - Expected source: `docs/browser-observation.md:200-208` requires malformed pending replies to poison the connection and correlated reply session IDs to match the selected session. `src/browser/CdpTargetSession.ts:33-39` obtains a flat session, and `src/browser/CdpObservationSession.ts:69-74` sends `Page.enable` within that session. - Failure source: The independent loopback real-WebSocket probe observed the omitted-ID reply and later command resolving. In `src/browser/CdpConnection.ts:240-260`, the correlated reply handler compares session IDs only when the reply contains that property, then accepts an object result. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete The focused verifier command exited 0 with four tests passing in 592 ms. Its assertions checked the current outcomes described above, rather than asserting that the documented rejection already works. The observation for the missing-ID case was: ```json {"kind":"omitted","reply":{"id":2,"result":{}},"outcome":{"status":"resolved","result":{}},"next":{"status":"resolved","result":{}}} ``` The supported browser-observation entry point opens an authorized target session and passes its session ID to the observation commands. The probe exercises that transport boundary directly. It does not run the public CLI/MCP workflow or a real Chrome-family browser. Numeric command correlation still works, and the experiment establishes no security-boundary violation. ## Restoration check For the same attached session and the same correlated reply without `sessionId`, observe `protocol_error` from the pending command and the next command on that connection. Preserve the control outcomes: matching-session replies succeed, foreign-session replies fail the connection, and commands without a selected session accept replies without a session ID. These results restore the documented transport contract without requiring a particular implementation. ## Runtime or environment Failure verified at a2d8b94dd50dab8d966239dd63ab84718aa8b4b4 and still present at 8b181716aca2cb467be499cba226abfb5ae19141; Node v26.10.0; npm 11.19.1; Vitest 5.0.3; locked dependencies; loopback HTTP/WebSocket fake browser; one focused source-test file. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A malformed correlated response is accepted as a successful session-scoped command result, and later commands continue despite the documented fatal wire failure. This is a bounded behavioral defect for peers that omit the reply session ID. No real-browser incident, wrong-target result, or population frequency was established. - Workaround status: unverified - Workaround basis: A matching-session reply succeeds in the local control, but no real-browser run or practical user workaround for an affected peer was verified.
bsakaguchi 2026-10-15 18:29:11 +00:00
Author
Owner

@morluto commented on GitHub (Oct 14, 2026):

Addressed on main in bffdd91053fb5d076b762cf1b325e8dcb5ab2d8c.

CDP command replies must preserve the selected session identity. The fake peer now echoes request identity through one ordinary reply owner.

Validation: Affected suites passed in the merged 65-file run (1169 passed, 2 explicit skips), with full static/architecture checks and documentation checks passing. Final installed-package verification is recorded separately in the audit.

Verification scope: Controlled source and production-boundary verification on macOS with pinned Node 24.18.0; real external engines and Windows hosts were not independently rerun. Closure covers the described source defect, not broader provider conformance or performance.

<!-- gh-comment-id:6159208768 --> @morluto commented on GitHub (Oct 14, 2026): Addressed on main in [bffdd91053fb5d076b762cf1b325e8dcb5ab2d8c](https://github.com/morluto/rea/commit/bffdd91053fb5d076b762cf1b325e8dcb5ab2d8c). CDP command replies must preserve the selected session identity. The fake peer now echoes request identity through one ordinary reply owner. Validation: Affected suites passed in the merged 65-file run (1169 passed, 2 explicit skips), with full static/architecture checks and documentation checks passing. Final installed-package verification is recorded separately in the audit. Verification scope: Controlled source and production-boundary verification on macOS with pinned Node 24.18.0; real external engines and Windows hosts were not independently rerun. Closure covers the described source defect, not broader provider conformance or performance.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#871
No description provided.