[GH-ISSUE #5665] [Bug] Malformed Page navigation frame identity is silently treated as non-main #3427

Closed
opened 2026-10-31 00:40:22 +00:00 by bsakaguchi · 0 comments
Owner

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

[Bug] Malformed Page navigation frame identity is silently treated as non-main

Before submitting

  • I searched open and closed issues and PRs for related reports.
  • I removed credentials and private target paths.

Area

Process capture or runtime observation

Observed behavior

Source-level fake-wire reproduction through the production CDP parser and CdpBrowserProvider.observeSession shows that a Page.frameRequestedNavigation event with a missing or numeric frameId is accepted, then silently omitted from the timeline. The result is ok: true, window.end_reason: "window_elapsed", and completeness.status: "attach_limited"; there is no protocol-value limitation. A valid main-frame event with the same outside-policy destination instead ends with target_left_scope and reports a redacted navigation_requested row. The same silent omission occurs for Page.frameNavigated with missing frame.id and Page.navigatedWithinDocument with missing frameId.

This is a malformed-input/source-boundary finding. It does not establish that Chromium emits these events or that a real main-frame navigation escaped origin enforcement.

Reproduction

Ran with Node.js v24.18.0 on Darwin arm64, package version 6.7.0 source, using the production source provider and repository fake CDP browser over loopback WebSocket. No target JavaScript was executed and no real browser was used.

import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import { observeWebSessionInputSchema } from "../../../src/domain/browserSession.js";
import { startFakeCdpBrowser } from "../../../tests/fixtures/fakeCdpBrowser.js";

const cases = [
  {
    name: "missing identity",
    event: {
      method: "Page.frameRequestedNavigation",
      params: { url: "https://outside.example/path", reason: "scriptInitiated" },
    },
  },
  {
    name: "malformed identity",
    event: {
      method: "Page.frameRequestedNavigation",
      params: { frameId: 7, url: "https://outside.example/path", reason: "scriptInitiated" },
    },
  },
  {
    name: "valid selected-frame control",
    event: {
      method: "Page.frameRequestedNavigation",
      params: { frameId: "frame-main", url: "https://outside.example/path", reason: "scriptInitiated" },
    },
  },
];

for (const { name, event } of cases) {
  const browser = await startFakeCdpBrowser({
    eventsBeforeReply: ["Performance.getMetrics"],
    commandResult: (command) =>
      command.method === "Performance.enable"
        ? {}
        : command.method === "Performance.getMetrics"
          ? { metrics: [{ name: "Timestamp", value: 20 }] }
          : undefined,
    commandEvents: (command) =>
      command.method === "Performance.getMetrics"
        ? [{ ...event, sessionId: command.sessionId }]
        : undefined,
  });
  try {
    const result = await new CdpBrowserProvider().observeSession(
      observeWebSessionInputSchema.parse({
        cdp_endpoint: browser.endpoint,
        allowed_origins: [browser.allowedOrigin],
        target_id: "allowed-page",
        observation_ms: 5,
      }),
    );
    process.stdout.write(`${name}: ${JSON.stringify(result)}\n`);
  } finally {
    await browser.close();
  }
}

Executed command:

node --import tsx .cache/codebase-audit/javascript5609-refresh60/frame-identity-wire-probe.mts

Observed output: each malformed case returns success, window_elapsed, an empty timeline, and attach_limited only. The valid selected-frame control returns target_left_scope, a navigation_requested event with destination_scope: "outside_policy", and no destination URL. The current-HEAD run output is preserved in frame-identity-wire-probe-head-0add6a5.log; the self-contained reproduction source above contains the decisive fake-wire setup and cases.

The source-only probe was first run against HEAD 1629266e3c08c90fed6bba5383528af7986a12b7 with the parent’s dirty #5609 CDP candidate, then rerun against clean HEAD 0add6a5aae2269199a6323556a092af91d525574. The rerun produced the same results. Current source hashes for the relevant path are recorded in frame-identity-fidelity-audit.md. The first run did not retain per-file hashes, so the earlier candidate cannot be reconstructed byte-for-byte from that run alone.

Expected behavior

The current DevTools Protocol schema marks Page.Frame.id, Page.frameRequestedNavigation.frameId, and Page.navigatedWithinDocument.frameId as required. A malformed event should remain explicit as unknown or malformed protocol evidence; it should not be converted to the definite fact “this is not the selected main frame.” This expectation is about REA’s handling of invalid input, not a claim that Chromium sends it.

References: Page.Frame.id, Page.frameNavigated, Page.frameRequestedNavigation, and Page.navigatedWithinDocument.

Impact and scope

Observed impact is loss of uncertainty: an event that cannot be associated with a particular frame vanishes without reducing the result’s completeness. The fake’s final frame tree remains on the allowed origin; therefore this does not prove an actual out-of-origin navigation, a policy escape, or false safe continuation after a real navigation. Real-browser incidence and practical impact are unknown. The public result’s attach_limited status remains present, but it describes the attach boundary and does not identify this malformed event.

REA version

Source revision 0add6a5aae2269199a6323556a092af91d525574 (package version 6.7.0). Earlier probe: source revision 1629266e3c08c90fed6bba5383528af7986a12b7 plus parent’s dirty #5609 CDP candidate; per-file hashes for that earlier state were not captured.

Runtime and platform

Node.js v24.18.0; macOS Darwin arm64. The reproduction uses a local fake CDP HTTP/WebSocket server. No installed Chrome/Chromium version applies; no real browser was exercised.

Target and artifact context

Repository fake CDP page target allowed-page, with synthetic selected frame ID frame-main; the fake final frame tree stays on browser.allowedOrigin. Event destinations use https://outside.example/path. No external target or JavaScript artifact was loaded or executed.

Interface

Other — direct source-level call to the production browser provider (CdpBrowserProvider.observeSession), not the public CLI or MCP adapter.

Earliest failure stage

Capture or observation. The generic CDP transport parser accepts the envelope, and the observer/helper then drops its unrecognized required frame identity.

Relevant output or evidence

The exact current-source probe command is shown above. On malformed identity, the relevant fields were:

{"ok":true,"end_reason":"window_elapsed","timeline":[],"completeness":{"status":"attach_limited","conditions":["attach_limited"],"attach_limited_sections":["timeline"],"truncated_sections":[],"unavailable_sections":[],"excluded":[]}}

On valid selected-frame control, output included end_reason: "target_left_scope", one navigation_requested timeline item with destination_scope: "outside_policy", and an out_of_target_scope exclusion. Full actual output is in frame-identity-wire-probe-head-0add6a5.log in the local audit cache; the code above is included so this report does not depend on that ignored cache file.

Typed outcome or recovery expected

Preserve a typed unknown/malformed identity outcome and mark the affected navigation evidence incomplete (for example, invalid_protocol_value or an equivalent explicit event-identity limitation). Do not classify unknown as main frame or as known child frame. Whether a given consumer should stop, continue with an explicit limitation, or use a separately verified current-frame identity requires a contract decision.

Analysis or suspected cause

Source trace: CdpConnection.#receive validates JSON and generic envelope shape but not method-specific Page event fields. cdpTargetEventMatches checks session routing only. isMainFrameNavigation reads the event-specific ID through cdpStringValue and returns a boolean comparison, so missing or wrongly typed IDs become false, indistinguishable from a valid other-frame ID. The observation-session navigation methods and normal capture/screenshot handlers use this boolean as an early filter.

The source-backed consumers include CdpObservationSession navigation requested/committed/same-document paths, CdpCaptureEventHandlers.handleFrameNavigated, and CdpScreenshot.observeScreenshotNavigation. CdpExecutionWindow contains a separate inline root-frame heuristic for Page.frameNavigated based on parentId; it does not use the shared helper and can interpret malformed objects differently. These are candidates for a single owner and explicit unknown state, not evidence of a reproduced runtime policy escape.

Additional context

Live dedupe: #5609 is still open and concerns pre-window lifecycle timestamps. PR #5639 fixes that lifecycle/window issue. Active PR #4040 is broader and touches relevant consumers, but its current file list does not modify CdpCaptureEventHelpers.ts or CdpTargetEvents.ts; the current helper remains boolean. Coordinate any later implementation with #4040 because its browser evidence/final-frame ownership changes overlap downstream consumers. Closed #2556 concerns malformed IDs in frame inventory, not malformed Page event identity. No exact existing report for this event-identity loss was found in the searched open/closed issue and PR results.

Prepared with AI assistance; source-boundary reproduction and its limitations are stated above. NOT_ENOUGH_INFO: first introducing revision and real-browser frequency.

Originally created by @morluto on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5665 # [Bug] Malformed Page navigation frame identity is silently treated as non-main ## Before submitting - [x] I searched open and closed issues and PRs for related reports. - [x] I removed credentials and private target paths. ## Area Process capture or runtime observation ## Observed behavior Source-level fake-wire reproduction through the production CDP parser and `CdpBrowserProvider.observeSession` shows that a `Page.frameRequestedNavigation` event with a missing or numeric `frameId` is accepted, then silently omitted from the timeline. The result is `ok: true`, `window.end_reason: "window_elapsed"`, and `completeness.status: "attach_limited"`; there is no protocol-value limitation. A valid main-frame event with the same outside-policy destination instead ends with `target_left_scope` and reports a redacted `navigation_requested` row. The same silent omission occurs for `Page.frameNavigated` with missing `frame.id` and `Page.navigatedWithinDocument` with missing `frameId`. This is a malformed-input/source-boundary finding. It does not establish that Chromium emits these events or that a real main-frame navigation escaped origin enforcement. ## Reproduction Ran with Node.js v24.18.0 on Darwin arm64, package version 6.7.0 source, using the production source provider and repository fake CDP browser over loopback WebSocket. No target JavaScript was executed and no real browser was used. ```ts import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; import { observeWebSessionInputSchema } from "../../../src/domain/browserSession.js"; import { startFakeCdpBrowser } from "../../../tests/fixtures/fakeCdpBrowser.js"; const cases = [ { name: "missing identity", event: { method: "Page.frameRequestedNavigation", params: { url: "https://outside.example/path", reason: "scriptInitiated" }, }, }, { name: "malformed identity", event: { method: "Page.frameRequestedNavigation", params: { frameId: 7, url: "https://outside.example/path", reason: "scriptInitiated" }, }, }, { name: "valid selected-frame control", event: { method: "Page.frameRequestedNavigation", params: { frameId: "frame-main", url: "https://outside.example/path", reason: "scriptInitiated" }, }, }, ]; for (const { name, event } of cases) { const browser = await startFakeCdpBrowser({ eventsBeforeReply: ["Performance.getMetrics"], commandResult: (command) => command.method === "Performance.enable" ? {} : command.method === "Performance.getMetrics" ? { metrics: [{ name: "Timestamp", value: 20 }] } : undefined, commandEvents: (command) => command.method === "Performance.getMetrics" ? [{ ...event, sessionId: command.sessionId }] : undefined, }); try { const result = await new CdpBrowserProvider().observeSession( observeWebSessionInputSchema.parse({ cdp_endpoint: browser.endpoint, allowed_origins: [browser.allowedOrigin], target_id: "allowed-page", observation_ms: 5, }), ); process.stdout.write(`${name}: ${JSON.stringify(result)}\n`); } finally { await browser.close(); } } ``` Executed command: ```sh node --import tsx .cache/codebase-audit/javascript5609-refresh60/frame-identity-wire-probe.mts ``` Observed output: each malformed case returns success, `window_elapsed`, an empty timeline, and `attach_limited` only. The valid selected-frame control returns `target_left_scope`, a `navigation_requested` event with `destination_scope: "outside_policy"`, and no destination URL. The current-HEAD run output is preserved in `frame-identity-wire-probe-head-0add6a5.log`; the self-contained reproduction source above contains the decisive fake-wire setup and cases. The source-only probe was first run against HEAD `1629266e3c08c90fed6bba5383528af7986a12b7` with the parent’s dirty #5609 CDP candidate, then rerun against clean HEAD `0add6a5aae2269199a6323556a092af91d525574`. The rerun produced the same results. Current source hashes for the relevant path are recorded in `frame-identity-fidelity-audit.md`. The first run did not retain per-file hashes, so the earlier candidate cannot be reconstructed byte-for-byte from that run alone. ## Expected behavior The current DevTools Protocol schema marks `Page.Frame.id`, `Page.frameRequestedNavigation.frameId`, and `Page.navigatedWithinDocument.frameId` as required. A malformed event should remain explicit as unknown or malformed protocol evidence; it should not be converted to the definite fact “this is not the selected main frame.” This expectation is about REA’s handling of invalid input, not a claim that Chromium sends it. References: [Page.Frame.id](https://github.com/ChromeDevTools/devtools-protocol/blob/master/pdl/domains/Page.pdl#L251-L263), [Page.frameNavigated](https://github.com/ChromeDevTools/devtools-protocol/blob/master/pdl/domains/Page.pdl#L1289-L1294), [Page.frameRequestedNavigation](https://github.com/ChromeDevTools/devtools-protocol/blob/master/pdl/domains/Page.pdl#L1330-L1341), and [Page.navigatedWithinDocument](https://github.com/ChromeDevTools/devtools-protocol/blob/master/pdl/domains/Page.pdl#L1645-L1659). ## Impact and scope Observed impact is loss of uncertainty: an event that cannot be associated with a particular frame vanishes without reducing the result’s completeness. The fake’s final frame tree remains on the allowed origin; therefore this does not prove an actual out-of-origin navigation, a policy escape, or false safe continuation after a real navigation. Real-browser incidence and practical impact are unknown. The public result’s `attach_limited` status remains present, but it describes the attach boundary and does not identify this malformed event. ## REA version Source revision `0add6a5aae2269199a6323556a092af91d525574` (package version 6.7.0). Earlier probe: source revision `1629266e3c08c90fed6bba5383528af7986a12b7` plus parent’s dirty #5609 CDP candidate; per-file hashes for that earlier state were not captured. ## Runtime and platform Node.js v24.18.0; macOS Darwin arm64. The reproduction uses a local fake CDP HTTP/WebSocket server. No installed Chrome/Chromium version applies; no real browser was exercised. ## Target and artifact context Repository fake CDP page target `allowed-page`, with synthetic selected frame ID `frame-main`; the fake final frame tree stays on `browser.allowedOrigin`. Event destinations use `https://outside.example/path`. No external target or JavaScript artifact was loaded or executed. ## Interface Other — direct source-level call to the production browser provider (`CdpBrowserProvider.observeSession`), not the public CLI or MCP adapter. ## Earliest failure stage Capture or observation. The generic CDP transport parser accepts the envelope, and the observer/helper then drops its unrecognized required frame identity. ## Relevant output or evidence The exact current-source probe command is shown above. On malformed identity, the relevant fields were: ```json {"ok":true,"end_reason":"window_elapsed","timeline":[],"completeness":{"status":"attach_limited","conditions":["attach_limited"],"attach_limited_sections":["timeline"],"truncated_sections":[],"unavailable_sections":[],"excluded":[]}} ``` On valid selected-frame control, output included `end_reason: "target_left_scope"`, one `navigation_requested` timeline item with `destination_scope: "outside_policy"`, and an `out_of_target_scope` exclusion. Full actual output is in `frame-identity-wire-probe-head-0add6a5.log` in the local audit cache; the code above is included so this report does not depend on that ignored cache file. ## Typed outcome or recovery expected Preserve a typed `unknown`/malformed identity outcome and mark the affected navigation evidence incomplete (for example, `invalid_protocol_value` or an equivalent explicit event-identity limitation). Do not classify unknown as main frame or as known child frame. Whether a given consumer should stop, continue with an explicit limitation, or use a separately verified current-frame identity requires a contract decision. ## Analysis or suspected cause Source trace: `CdpConnection.#receive` validates JSON and generic envelope shape but not method-specific Page event fields. `cdpTargetEventMatches` checks session routing only. `isMainFrameNavigation` reads the event-specific ID through `cdpStringValue` and returns a boolean comparison, so missing or wrongly typed IDs become `false`, indistinguishable from a valid other-frame ID. The observation-session navigation methods and normal capture/screenshot handlers use this boolean as an early filter. The source-backed consumers include `CdpObservationSession` navigation requested/committed/same-document paths, `CdpCaptureEventHandlers.handleFrameNavigated`, and `CdpScreenshot.observeScreenshotNavigation`. `CdpExecutionWindow` contains a separate inline root-frame heuristic for `Page.frameNavigated` based on `parentId`; it does not use the shared helper and can interpret malformed objects differently. These are candidates for a single owner and explicit unknown state, not evidence of a reproduced runtime policy escape. ## Additional context Live dedupe: #5609 is still open and concerns pre-window lifecycle timestamps. PR #5639 fixes that lifecycle/window issue. Active PR [#4040](https://github.com/morluto/rea/pull/4040) is broader and touches relevant consumers, but its current file list does not modify `CdpCaptureEventHelpers.ts` or `CdpTargetEvents.ts`; the current helper remains boolean. Coordinate any later implementation with #4040 because its browser evidence/final-frame ownership changes overlap downstream consumers. Closed #2556 concerns malformed IDs in frame inventory, not malformed Page event identity. No exact existing report for this event-identity loss was found in the searched open/closed issue and PR results. Prepared with AI assistance; source-boundary reproduction and its limitations are stated above. NOT_ENOUGH_INFO: first introducing revision and real-browser frequency.
bsakaguchi 2026-10-31 00:40:22 +00:00
  • closed this issue
  • added the
    bug
    triage
    labels
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#3427
No description provided.