[GH-ISSUE #3960] [Bug] Web capture inspection accepts empty containers at nesting level 65 despite its 64-level limit #1567

Closed
opened 2026-10-16 21:07:15 +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/3960

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

inspect-web-network-capture returns successful Evidence for qlog and HAR captures containing an extension chain with an empty container at nesting level 65. It preserves the extension even though complete evidence promises a maximum of 64 container levels. The corresponding level-66 input fails. The observed discrepancy is limited to one additional container level, with no consequential parsing or resource failure observed.

Reproduction

From the baseline checkout with its installed dependencies and built runtime, run the following script with the pinned Node.js runtime. It creates disposable fixtures and an isolated HOME, invokes the supported CLI, prints each result, and deletes its generated files. The document root counts as container level 1.

node --input-type=module <<'NODE'
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
const work = mkdtempSync(join(process.env.VERIFY_SCRATCH ?? tmpdir(), 'rea-nesting-'));
const home = join(work, 'home');
mkdirSync(home);
const cases = [];
for (const format of ['qlog', 'har'])
  for (const depth of [64, 65, 66])
    cases.push({ format, depth, kind: 'empty-array' });
for (const depth of [64, 65])
  cases.push({ format: 'qlog', depth, kind: 'scalar' });
cases.push({ format: 'qlog', depth: 65, kind: 'empty-object' });
try {
  for (const { format, depth, kind } of cases) {
    const count = depth - (format === 'qlog' ? 3 : 2);
    const extension = kind === 'empty-object'
      ? '{"child":'.repeat(count - 1) + '{}' + '}'.repeat(count - 1)
      : '['.repeat(count) + (kind === 'scalar' ? '0' : '') + ']'.repeat(count);
    const text = format === 'qlog'
      ? '{"qlog_format":"JSON","qlog_version":"0.3","traces":[{"events":[],"ext":' + extension + '}]}'
      : '{"log":{"version":"1.2","creator":{"name":"fixture","version":"1"},"entries":[],"_extension":' + extension + '}}';
    const file = join(work, `${format}-${kind}-${depth}.json`);
    writeFileSync(file, text);
    const result = spawnSync(process.execPath, [
      'scripts/rea.mjs', 'inspect-web-network-capture', file, format, '--json'
    ], { encoding: 'utf8', env: { ...process.env, HOME: home }, timeout: 45000 });
    if (result.error) throw result.error;
    const reply = JSON.parse(result.stdout);
    console.log(JSON.stringify({
      format, kind, depth, exit: result.status,
      outcome: reply.code ?? 'success',
      total_records: reply.normalized_result?.total_records,
      reason: reply.details?.issues?.[0]?.reason,
      maximum_capture_nesting: reply.details?.issues?.[0]?.expected?.maximum_capture_nesting
    }));
  }
} finally {
  rmSync(work, { recursive: true, force: true });
}
NODE

The qlog fixture has three enclosing containers before its extension. The HAR fixture has two. The scalar control keeps the same container count but adds a numeric zero to the innermost array.

Expected behavior

Complete evidence has a 64-level nesting budget, and exceeding that budget must return no partial success. Each depth-65 and depth-66 case should return the nesting-limit input error regardless of whether the innermost container is empty. Depth-64 cases should continue to succeed. Unknown extensions remain valid capture fields within the limit.

Impact and scope

The observed behavior admits exactly one extra container level beyond a documented limit. Deeper captures remain rejected, and these executions showed no crash, data loss, incorrect record interpretation, or consequential resource failure.

Workaround: Caller-generated captures can stay within 64 container levels. The depth-64 controls succeeded and preserved their extensions through the same CLI.

Assessment: P3.

REA version

rea 6.5.0 at source commit 4692bb3330

Runtime and platform

Node 24.18.0; macOS arm64

Target and artifact context

Locally generated JSON qlog 0.3 and HAR 1.2 captures with unknown extension fields. No live browser or external provider was used.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

Observed through the CLI with normal owned-process cleanup. Qlog and HAR each accepted the empty-array cases at depths 64 and 65, exited 0, returned zero records, and preserved the exact extension in normalized_result.container.reported. Both rejected depth 66, exited 1, and returned invalid_request with issue reason out_of_range and maximum_capture_nesting: 64. The qlog empty-object case at depth 65 also succeeded. Qlog scalar controls succeeded at depth 64 and failed at depth 65. All nine CLI invocations had empty stderr. Fixture depths and retained extension equality were checked independently.

Source-derived explanation: the shared JSON visitor checks the already-open container count before opening another container and checks again for literals. At 64 open containers it admits the next container; an empty innermost container closes without another depth check. A subsequent container opening or literal triggers rejection. This explains the difference between the empty and scalar controls.

Observed output for the discriminating input and its controls:

qlog empty-array 64: exit 0, success, total_records 0
qlog empty-array 65: exit 0, success, total_records 0
qlog empty-array 66: exit 1, invalid_request, out_of_range, maximum_capture_nesting 64
har empty-array 64: exit 0, success, total_records 0
har empty-array 65: exit 0, success, total_records 0
har empty-array 66: exit 1, invalid_request, out_of_range, maximum_capture_nesting 64
qlog scalar 64: exit 0, success, total_records 0
qlog scalar 65: exit 1, invalid_request, out_of_range, maximum_capture_nesting 64
qlog empty-object 65: exit 0, success, total_records 0

Typed outcome or recovery expected

Repeat the same CLI cases. The empty-array cases at depth 65 and the qlog empty-object case at depth 65 must return the nesting-limit input error with no successful Evidence. Depth 64 must still succeed and preserve the extension. Depth 66 and the depth-65 scalar control must remain rejected. These controls distinguish enforcing the documented boundary from rejecting all extensions or changing the advertised limit.

Additional context

Freshness checked against origin/main at 22ec8bdb2f. The cited failing implementation files are unchanged from the reproduced baseline; this is a source freshness check, not a runtime rerun on that revision.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3960 ### 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 `inspect-web-network-capture` returns successful Evidence for qlog and HAR captures containing an extension chain with an empty container at nesting level 65. It preserves the extension even though complete evidence promises a maximum of 64 container levels. The corresponding level-66 input fails. The observed discrepancy is limited to one additional container level, with no consequential parsing or resource failure observed. ### Reproduction From the baseline checkout with its installed dependencies and built runtime, run the following script with the pinned Node.js runtime. It creates disposable fixtures and an isolated HOME, invokes the supported CLI, prints each result, and deletes its generated files. The document root counts as container level 1. ```sh node --input-type=module <<'NODE' import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { spawnSync } from 'node:child_process'; const work = mkdtempSync(join(process.env.VERIFY_SCRATCH ?? tmpdir(), 'rea-nesting-')); const home = join(work, 'home'); mkdirSync(home); const cases = []; for (const format of ['qlog', 'har']) for (const depth of [64, 65, 66]) cases.push({ format, depth, kind: 'empty-array' }); for (const depth of [64, 65]) cases.push({ format: 'qlog', depth, kind: 'scalar' }); cases.push({ format: 'qlog', depth: 65, kind: 'empty-object' }); try { for (const { format, depth, kind } of cases) { const count = depth - (format === 'qlog' ? 3 : 2); const extension = kind === 'empty-object' ? '{"child":'.repeat(count - 1) + '{}' + '}'.repeat(count - 1) : '['.repeat(count) + (kind === 'scalar' ? '0' : '') + ']'.repeat(count); const text = format === 'qlog' ? '{"qlog_format":"JSON","qlog_version":"0.3","traces":[{"events":[],"ext":' + extension + '}]}' : '{"log":{"version":"1.2","creator":{"name":"fixture","version":"1"},"entries":[],"_extension":' + extension + '}}'; const file = join(work, `${format}-${kind}-${depth}.json`); writeFileSync(file, text); const result = spawnSync(process.execPath, [ 'scripts/rea.mjs', 'inspect-web-network-capture', file, format, '--json' ], { encoding: 'utf8', env: { ...process.env, HOME: home }, timeout: 45000 }); if (result.error) throw result.error; const reply = JSON.parse(result.stdout); console.log(JSON.stringify({ format, kind, depth, exit: result.status, outcome: reply.code ?? 'success', total_records: reply.normalized_result?.total_records, reason: reply.details?.issues?.[0]?.reason, maximum_capture_nesting: reply.details?.issues?.[0]?.expected?.maximum_capture_nesting })); } } finally { rmSync(work, { recursive: true, force: true }); } NODE ``` The qlog fixture has three enclosing containers before its extension. The HAR fixture has two. The scalar control keeps the same container count but adds a numeric zero to the innermost array. ### Expected behavior Complete evidence has a 64-level nesting budget, and exceeding that budget must return no partial success. Each depth-65 and depth-66 case should return the nesting-limit input error regardless of whether the innermost container is empty. Depth-64 cases should continue to succeed. Unknown extensions remain valid capture fields within the limit. ### Impact and scope The observed behavior admits exactly one extra container level beyond a documented limit. Deeper captures remain rejected, and these executions showed no crash, data loss, incorrect record interpretation, or consequential resource failure. Workaround: Caller-generated captures can stay within 64 container levels. The depth-64 controls succeeded and preserved their extensions through the same CLI. Assessment: P3. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860 ### Runtime and platform Node 24.18.0; macOS arm64 ### Target and artifact context Locally generated JSON qlog 0.3 and HAR 1.2 captures with unknown extension fields. No live browser or external provider was used. ### Interface CLI ### Earliest failure stage Request parsing or validation ### Relevant output or evidence Observed through the CLI with normal owned-process cleanup. Qlog and HAR each accepted the empty-array cases at depths 64 and 65, exited 0, returned zero records, and preserved the exact extension in `normalized_result.container.reported`. Both rejected depth 66, exited 1, and returned `invalid_request` with issue reason `out_of_range` and `maximum_capture_nesting: 64`. The qlog empty-object case at depth 65 also succeeded. Qlog scalar controls succeeded at depth 64 and failed at depth 65. All nine CLI invocations had empty stderr. Fixture depths and retained extension equality were checked independently. Source-derived explanation: the shared JSON visitor checks the already-open container count before opening another container and checks again for literals. At 64 open containers it admits the next container; an empty innermost container closes without another depth check. A subsequent container opening or literal triggers rejection. This explains the difference between the empty and scalar controls. - Expected source: [docs/web-network-captures.md:318-321](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/docs/web-network-captures.md#L318) specifies the 64-level budget and no partial success; [docs/web-network-captures.md:377-384](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/docs/web-network-captures.md#L377) permits unknown qlog extensions and requires excessive nesting to fail; [src/domain/webNetworkCapture.ts:6-12](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/src/domain/webNetworkCapture.ts#L6) defines depth 64. - Failure source: independently executed CLI reproduction above; [src/browser/history/CaptureJson.ts:32-39,56-62,80-97](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/src/browser/history/CaptureJson.ts#L32) supplies the source-derived explanation; [src/browser/history/QlogCapture.ts:33-45](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/src/browser/history/QlogCapture.ts#L33) and [src/browser/history/HarCapture.ts:29-48](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/src/browser/history/HarCapture.ts#L29) show both supported decoders calling the shared visitor. Observed output for the discriminating input and its controls: ```text qlog empty-array 64: exit 0, success, total_records 0 qlog empty-array 65: exit 0, success, total_records 0 qlog empty-array 66: exit 1, invalid_request, out_of_range, maximum_capture_nesting 64 har empty-array 64: exit 0, success, total_records 0 har empty-array 65: exit 0, success, total_records 0 har empty-array 66: exit 1, invalid_request, out_of_range, maximum_capture_nesting 64 qlog scalar 64: exit 0, success, total_records 0 qlog scalar 65: exit 1, invalid_request, out_of_range, maximum_capture_nesting 64 qlog empty-object 65: exit 0, success, total_records 0 ``` ### Typed outcome or recovery expected Repeat the same CLI cases. The empty-array cases at depth 65 and the qlog empty-object case at depth 65 must return the nesting-limit input error with no successful Evidence. Depth 64 must still succeed and preserve the extension. Depth 66 and the depth-65 scalar control must remain rejected. These controls distinguish enforcing the documented boundary from rejecting all extensions or changing the advertised limit. ### Additional context Freshness checked against origin/main at 22ec8bdb2f7d4626587f23b1eed4a207df600386. The cited failing implementation files are unchanged from the reproduced baseline; this is a source freshness check, not a runtime rerun on that revision.
Author
Owner

@Anurag-5ingh commented on GitHub (Oct 15, 2026):

I opened #3975 with a fix: the shared HAR/qlog parser now rejects any container at level 65 or deeper, whether or not it is empty, with boundary tests at 64/65/66.

<!-- gh-comment-id:6186177192 --> @Anurag-5ingh commented on GitHub (Oct 15, 2026): I opened #3975 with a fix: the shared HAR/qlog parser now rejects any container at level 65 or deeper, whether or not it is empty, with boundary tests at 64/65/66.
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#1567
No description provided.