[GH-ISSUE #3928] [Bug] capture-web-screenshot accepts truncated IHDR bytes as a successful PNG #1536

Closed
opened 2026-10-16 21:06:49 +00:00 by bsakaguchi · 1 comment
Owner

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

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

capture-web-screenshot returns successful screenshot Evidence when the selected CDP endpoint supplies a PNG prefix containing only part of the IHDR chunk. In the reproduced case, the caller receives 24 bytes labeled image/png, a 1 by 1 viewport and complete_within_window, although those bytes cannot contain a complete PNG header or image. This was reproduced through the CLI and the repository's simulated CDP producer over real HTTP/WebSocket transport, without a live browser.

Observed through the CLI, the 24-byte payload exited with code 0 and returned operation: capture_web_screenshot. Its normalized result contained viewport: {width: 1, height: 1}, an artifact with bytes: 24, media_type: image/png, data_base64: iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB, and completeness.status: complete_within_window. The 32-byte payload also exited with code 0 and retained all 32 bytes as successful screenshot evidence. The 23-byte control exited with code 1, code: unreadable_output and details.reason: protocol_error. The complete 68-byte fixture exited with code 0.

Source-derived reasoning explains the boundary: the shared header reader requires only the signature, IHDR length/type and width/height, which fit within 24 bytes. It does not require the rest of the declared IHDR data and CRC. The browser capture uses that result to admit the artifact, whose output schema verifies the digest and size without validating PNG structure. A separate direct schema replay also accepted the same prefixes as native screenshot results; that replay does not establish a real native-capture failure.

Reproduction

Use the repository's pinned toolchain, installed dependencies and built runtime. Save the following temporary test as tests/boundary/cli/screenshotHeaderReproduction.test.ts. The fixture supplies normal discovery, attachment and frame-tree replies, changing only the screenshot command's payload. Each CLI invocation selects the fixture endpoint and its exact origin.

import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { expect, it } from "vitest";
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";

const execute = promisify(execFile);
it("records CLI handling of incomplete screenshot IHDR data", async () => {
  const full = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jv4sAAAAASUVORK5CYII=", "base64");
  for (const n of [23, 24, 32, 33, full.length]) {
    const bytes = full.subarray(0, n);
    const browser = await startFakeCdpBrowser({
      commandResult: ({ method }) => method === "Page.captureScreenshot" ? { data: bytes.toString("base64") } : undefined,
    });
    try {
      let stdout: string;
      let exitCode = 0;
      try {
        ({ stdout } = await execute(process.execPath, ["scripts/rea.mjs", "capture-web-screenshot", browser.endpoint, "allowed-page", "--allowed-origins", browser.allowedOrigin, "--json"], { env: process.env, maxBuffer: 1024 * 1024 }));
      } catch (cause: any) {
        stdout = cause.stdout;
        exitCode = cause.code;
      }
      const result = JSON.parse(stdout);
      console.log(JSON.stringify({ n, exitCode, result, methods: browser.commands.map(c => c.method) }));
      if (n === 23) {
        expect(result).toMatchObject({ details: { reason: "protocol_error" } });
      } else {
        expect(result.normalized_result.viewport).toEqual({ width: 1, height: 1 });
        expect(result.normalized_result.artifact.bytes).toBe(n);
        expect(result.normalized_result.artifact.data_base64).toBe(bytes.toString("base64"));
        expect(exitCode).toBe(0);
      }
    } finally {
      await browser.close();
    }
  }
}, 60000);

Run the focused test with an isolated home directory, then remove the temporary test and home directory:

VERIFY_HOME="$(mktemp -d)"
HOME="$VERIFY_HOME" npx vitest run tests/boundary/cli/screenshotHeaderReproduction.test.ts
rm tests/boundary/cli/screenshotHeaderReproduction.test.ts
rm -r "$VERIFY_HOME"

The executed verification used an equivalent temporary filename and an isolated home directory. One focused test executed five CLI child processes. The 23-byte control failed as malformed output; 24-byte and 32-byte incomplete headers succeeded; the 33-byte header-only prefix and complete 68-byte fixture also succeeded. The test above records current behavior rather than an assertion of repaired behavior.

Expected behavior

The screenshot command promises a content-addressed visible-viewport PNG. A producer reply missing part of its declared IHDR data or CRC cannot satisfy that contract. It should produce the existing malformed-protocol failure rather than successful screenshot Evidence. The complete fixture must continue to succeed, with its original bytes and matching dimensions preserved.

Impact and scope

A supported CLI capture returns unusable screenshot bytes as successful, complete evidence when its CDP producer supplies an incomplete IHDR chunk. This is a bounded behavioral failure, not cosmetic cleanup. The reproduction uses a simulated producer; occurrence with a real browser is unknown.

Workaround: Independent caller-side PNG validation could detect malformed artifacts, but that workflow was not executed or established as a practical replacement for the command's output validation. No stricter screenshot-validation option was found in the documented CLI help.

REA version

rea 6.5.0 at source commit 4692bb3330

Runtime and platform

Node 24.18.0; macOS arm64

Target and artifact context

Repository wire-level CDP fixture over HTTP/WebSocket; 1 by 1 PNG prefixes. No live browser or native capture was exercised.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

  • Expected source: docs/browser-observation.md:34 promises a content-addressed visible-viewport PNG; src/contracts/browserToolContracts.ts:263-269 advertises an inline PNG artifact. The existing malformed-screenshot producer tests in tests/boundary/browser/cdpScreenshotDocumentIdentity.test.ts:7-59 establish the protocol-error behavior for rejected producer replies.
  • Failure source: The executed CLI reproduction above returned success for the 24-byte and 32-byte prefixes. src/domain/pngHeader.ts:8-16 admits a buffer once width and height are present; src/browser/CdpScreenshot.ts:93-126 uses that admission before returning the screenshot artifact. src/domain/webScreenshot.ts:13-38 verifies artifact base64, digest and byte count without a later PNG-structure guard.

The fixture's producer-reply seam preserves the real HTTP/WebSocket transport. The recorded command sequence included target attachment, page enablement, a frame-tree read, screenshot capture, a second frame-tree read and session cleanup. Thus this reproduction includes the supported command's discovery, authorization, transport, output validation and Evidence projection rather than only the internal parser.

Typed outcome or recovery expected

Repeat the same producer replies through the CLI. The 24-byte and 32-byte cases must report malformed screenshot output and must not return successful PNG Evidence. Keep the 23-byte malformed control rejected and the complete 68-byte fixture successful with its bytes and dimensions preserved. Acceptance of a 33-byte header-only buffer is an observation of the baseline, not a compatibility requirement. No real native or browser producer failure is asserted; any such claim requires its own provider verification.

Analysis or suspected cause

Source-derived explanation and exact locators are in Relevant output or evidence. Provider and transport limits are stated there separately from reproduced results.

Additional context

Occurrence with a real browser is unknown. The complete PNG is the success control; header-only acceptance is not a compatibility requirement.

Freshness check: the cited source and contract files have no changes between the tested source commit and upstream main at 22ec8bdb2f.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3928 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior `capture-web-screenshot` returns successful screenshot Evidence when the selected CDP endpoint supplies a PNG prefix containing only part of the IHDR chunk. In the reproduced case, the caller receives 24 bytes labeled `image/png`, a 1 by 1 viewport and `complete_within_window`, although those bytes cannot contain a complete PNG header or image. This was reproduced through the CLI and the repository's simulated CDP producer over real HTTP/WebSocket transport, without a live browser. Observed through the CLI, the 24-byte payload exited with code 0 and returned `operation: capture_web_screenshot`. Its normalized result contained `viewport: {width: 1, height: 1}`, an artifact with `bytes: 24`, `media_type: image/png`, `data_base64: iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB`, and `completeness.status: complete_within_window`. The 32-byte payload also exited with code 0 and retained all 32 bytes as successful screenshot evidence. The 23-byte control exited with code 1, `code: unreadable_output` and `details.reason: protocol_error`. The complete 68-byte fixture exited with code 0. Source-derived reasoning explains the boundary: the shared header reader requires only the signature, IHDR length/type and width/height, which fit within 24 bytes. It does not require the rest of the declared IHDR data and CRC. The browser capture uses that result to admit the artifact, whose output schema verifies the digest and size without validating PNG structure. A separate direct schema replay also accepted the same prefixes as native screenshot results; that replay does not establish a real native-capture failure. ### Reproduction Use the repository's pinned toolchain, installed dependencies and built runtime. Save the following temporary test as `tests/boundary/cli/screenshotHeaderReproduction.test.ts`. The fixture supplies normal discovery, attachment and frame-tree replies, changing only the screenshot command's payload. Each CLI invocation selects the fixture endpoint and its exact origin. ```ts import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { expect, it } from "vitest"; import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; const execute = promisify(execFile); it("records CLI handling of incomplete screenshot IHDR data", async () => { const full = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jv4sAAAAASUVORK5CYII=", "base64"); for (const n of [23, 24, 32, 33, full.length]) { const bytes = full.subarray(0, n); const browser = await startFakeCdpBrowser({ commandResult: ({ method }) => method === "Page.captureScreenshot" ? { data: bytes.toString("base64") } : undefined, }); try { let stdout: string; let exitCode = 0; try { ({ stdout } = await execute(process.execPath, ["scripts/rea.mjs", "capture-web-screenshot", browser.endpoint, "allowed-page", "--allowed-origins", browser.allowedOrigin, "--json"], { env: process.env, maxBuffer: 1024 * 1024 })); } catch (cause: any) { stdout = cause.stdout; exitCode = cause.code; } const result = JSON.parse(stdout); console.log(JSON.stringify({ n, exitCode, result, methods: browser.commands.map(c => c.method) })); if (n === 23) { expect(result).toMatchObject({ details: { reason: "protocol_error" } }); } else { expect(result.normalized_result.viewport).toEqual({ width: 1, height: 1 }); expect(result.normalized_result.artifact.bytes).toBe(n); expect(result.normalized_result.artifact.data_base64).toBe(bytes.toString("base64")); expect(exitCode).toBe(0); } } finally { await browser.close(); } } }, 60000); ``` Run the focused test with an isolated home directory, then remove the temporary test and home directory: ```sh VERIFY_HOME="$(mktemp -d)" HOME="$VERIFY_HOME" npx vitest run tests/boundary/cli/screenshotHeaderReproduction.test.ts rm tests/boundary/cli/screenshotHeaderReproduction.test.ts rm -r "$VERIFY_HOME" ``` The executed verification used an equivalent temporary filename and an isolated home directory. One focused test executed five CLI child processes. The 23-byte control failed as malformed output; 24-byte and 32-byte incomplete headers succeeded; the 33-byte header-only prefix and complete 68-byte fixture also succeeded. The test above records current behavior rather than an assertion of repaired behavior. ### Expected behavior The screenshot command promises a content-addressed visible-viewport PNG. A producer reply missing part of its declared IHDR data or CRC cannot satisfy that contract. It should produce the existing malformed-protocol failure rather than successful screenshot Evidence. The complete fixture must continue to succeed, with its original bytes and matching dimensions preserved. ### Impact and scope A supported CLI capture returns unusable screenshot bytes as successful, complete evidence when its CDP producer supplies an incomplete IHDR chunk. This is a bounded behavioral failure, not cosmetic cleanup. The reproduction uses a simulated producer; occurrence with a real browser is unknown. Workaround: Independent caller-side PNG validation could detect malformed artifacts, but that workflow was not executed or established as a practical replacement for the command's output validation. No stricter screenshot-validation option was found in the documented CLI help. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860 ### Runtime and platform Node 24.18.0; macOS arm64 ### Target and artifact context Repository wire-level CDP fixture over HTTP/WebSocket; 1 by 1 PNG prefixes. No live browser or native capture was exercised. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence - Expected source: `docs/browser-observation.md:34` promises a content-addressed visible-viewport PNG; `src/contracts/browserToolContracts.ts:263-269` advertises an inline PNG artifact. The existing malformed-screenshot producer tests in `tests/boundary/browser/cdpScreenshotDocumentIdentity.test.ts:7-59` establish the protocol-error behavior for rejected producer replies. - Failure source: The executed CLI reproduction above returned success for the 24-byte and 32-byte prefixes. `src/domain/pngHeader.ts:8-16` admits a buffer once width and height are present; `src/browser/CdpScreenshot.ts:93-126` uses that admission before returning the screenshot artifact. `src/domain/webScreenshot.ts:13-38` verifies artifact base64, digest and byte count without a later PNG-structure guard. The fixture's producer-reply seam preserves the real HTTP/WebSocket transport. The recorded command sequence included target attachment, page enablement, a frame-tree read, screenshot capture, a second frame-tree read and session cleanup. Thus this reproduction includes the supported command's discovery, authorization, transport, output validation and Evidence projection rather than only the internal parser. ### Typed outcome or recovery expected Repeat the same producer replies through the CLI. The 24-byte and 32-byte cases must report malformed screenshot output and must not return successful PNG Evidence. Keep the 23-byte malformed control rejected and the complete 68-byte fixture successful with its bytes and dimensions preserved. Acceptance of a 33-byte header-only buffer is an observation of the baseline, not a compatibility requirement. No real native or browser producer failure is asserted; any such claim requires its own provider verification. ### Analysis or suspected cause Source-derived explanation and exact locators are in Relevant output or evidence. Provider and transport limits are stated there separately from reproduced results. ### Additional context Occurrence with a real browser is unknown. The complete PNG is the success control; header-only acceptance is not a compatibility requirement. Freshness check: the cited source and contract files have no changes between the tested source commit and upstream main at 22ec8bdb2f7d4626587f23b1eed4a207df600386.
Author
Owner

@XinwangTao commented on GitHub (Oct 15, 2026):

Confirmed: a 24-byte prefix labeled image/png cannot be a usable screenshot, and returning complete_within_window for it masks a broken producer. The header reader should validate the whole IHDR chunk (declared length satisfied, CRC valid) before admitting a PNG — otherwise truncated CDP output is silently promoted to successful Evidence.

<!-- gh-comment-id:6186335641 --> @XinwangTao commented on GitHub (Oct 15, 2026): Confirmed: a 24-byte prefix labeled image/png cannot be a usable screenshot, and returning complete_within_window for it masks a broken producer. The header reader should validate the whole IHDR chunk (declared length satisfied, CRC valid) before admitting a PNG — otherwise truncated CDP output is silently promoted to successful Evidence.
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#1536
No description provided.