[GH-ISSUE #3212] [Bug] ContextBridge object spreads omit explicit nested API members #1149

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

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

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

analyze-javascript-application omits an explicit nested contextBridge member when an inline API object contains an unknown spread. For { ...extra, a: { b: 1 }, c: 2 }, the API observation lists a and c but loses a.b, and summary.exposed_api_members is 2 instead of 3. The literal definitions follow the spread, so that spread cannot overwrite them. The no-spread control retains all three members. Both observed CLI commands exited 0 and returned their results under normalized_result.

Reproduction

  1. Start from a clean checkout of revision 58300885207133422af235e0f2b10658cf958b03 with the repository's pinned development toolchain. From the repository root, install the locked dependencies and build the CLI:

    npm ci
    npm run build:cached
    

    Verification used prepared locked dependencies and built output; it did not rerun these setup commands.

  2. Create two inert application inputs and an isolated home for the CLI subprocesses:

    mkdir -p repro-work/home repro-work/with-spread repro-work/control
    cat > repro-work/with-spread/preload.js <<'JS'
    const { contextBridge } = require("electron");
    const extra = getExtra();
    contextBridge.exposeInMainWorld("api", { ...extra, a: { b: 1 }, c: 2 });
    JS
    cat > repro-work/control/preload.js <<'JS'
    const { contextBridge } = require("electron");
    contextBridge.exposeInMainWorld("api", { a: { b: 1 }, c: 2 });
    JS
    
  3. Analyze each directory with its source file explicitly selected. REA parses the fixture as inert source; it does not execute Electron or getExtra():

    repro_home="$PWD/repro-work/home"
    env HOME="$repro_home" XDG_CONFIG_HOME="$repro_home/config" \
      XDG_CACHE_HOME="$repro_home/cache" XDG_DATA_HOME="$repro_home/data" \
      node scripts/rea.mjs analyze-javascript-application ./repro-work/with-spread \
      --source-path preload.js --format json > repro-work/with-spread.json
    env HOME="$repro_home" XDG_CONFIG_HOME="$repro_home/config" \
      XDG_CACHE_HOME="$repro_home/cache" XDG_DATA_HOME="$repro_home/data" \
      node scripts/rea.mjs analyze-javascript-application ./repro-work/control \
      --source-path preload.js --format json > repro-work/control.json
    
  4. Print the returned API member lists and counts:

    node --input-type=module <<'JS'
    import fs from "node:fs";
    for (const name of ["with-spread", "control"]) {
      const result = JSON.parse(fs.readFileSync(`repro-work/${name}.json`, "utf8")).normalized_result;
      console.log(name, "exposed_api_members", result.summary.exposed_api_members);
      for (const node of result.graph.nodes) {
        if (node.kind !== "context-bridge-api") continue;
        for (const observation of node.observations) {
          console.log(JSON.stringify(observation.properties));
        }
      }
    }
    JS
    

Expected behavior

docs/javascript-artifact-reconstruction.md:229-230 documents contextBridge API keys and bounded literal member paths. The spread-first input has the same explicit a.b path as the control, within the supported depth bound. Its API observation should retain a, a.b, and c, and its summary should count those three retained members. The unresolved spread contribution should remain explicitly unknown. Retaining the later literal members must not imply exact knowledge of additional spread-contributed properties or runtime execution. AGENTS.md:29 also requires preserving partial facts.

Impact and scope

The successful analysis silently returns an incomplete member list and an undercount for the reproduced input. The API key, top-level members, and spread uncertainty remain available. Verification covered a local extracted application directory and an explicitly selected JavaScript file through the CLI. ASAR and MCP behavior were not replayed. No Electron process or native analysis engine was required, and no security boundary crossing was observed.

Removing the spread restores the control output but changes the analyzed artifact. No caller option or alternate supported entry point that preserves the original input and returns the missing graph member was verified.

REA version

6.4.0 (main at 5830088520)

Runtime and platform

Node.js v26.10.0; macOS. The verifier did not record the OS version or architecture.

Target and artifact context

Local extracted application directory containing one explicitly selected preload.js file. The fixture contains an inline contextBridge API object with an unresolved leading spread followed by explicit literal properties. The control omits the spread.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The observed member fields and summary counts were:

with-spread: members=["a","c"], unknown_members=1, exposed_api_members=2
control:     members=["a","a.b","c"], unknown_members=0, exposed_api_members=3

The spread observation also returned api_status: "dynamic" and resolution_basis: "wrapper-substitution". The control returned api_status: "object-literal" and resolution_basis: "direct-syntax". Those fields provide observed context; this report does not require a particular replacement status or provenance implementation.

Typed outcome or recovery expected

Return a successful analysis whose API observation retains a, a.b, and c, whose exposed-member count is 3, and whose unresolved spread remains explicitly uncertain. Keep the no-spread control's three-member list and zero unknown members. Suppressing the API observation, dropping nested paths from the control, or treating the unknown spread as exact would not restore the contract.

Analysis or suspected cause

Source-derived reasoning at the stated baseline explains the observed loss. src/domain/javascript/electronStaticAnalysisValues.ts:104-167 recursively collects literal dotted member paths while separately counting spread uncertainty. src/domain/javascript/electronStaticAnalysisBrowser.ts:178-207 first records that literal result.

src/domain/javascript/electronWrappedCallPaths.ts:400-414 constructs a replacement exposure finding using resolver.members(object). The object branch at src/domain/javascript/javascriptSymbolicBindings.ts:342-354 enumerates top-level keys and spreads without recursively retaining nested literal paths. The replacement at src/domain/javascript/electronWrappedCallPaths.ts:432-439 substitutes that finding when the original has unknown members.

src/application/javascript/ElectronBoundaryGraphWindows.ts:201 copies the retained member list into the graph, and src/application/javascript/ElectronBoundaryAnalysis.ts:130-133 counts its length. The incomplete graph member list and summary count are downstream effects of that substitution. These locators explain the failure without prescribing a repair.

Additional context

Related closed issue #2669 concerns missing candidate call paths for nested bridge functions whose member names already remain in the graph; this report concerns a nested literal member missing from the list itself when an unknown spread is present.

The supported impact rating is P2 because one bounded API observation and its summary count are incomplete while analysis succeeds and retains other facts. A practical workaround that preserves the original artifact and returned member list remains unverified.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3212 ### 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 `analyze-javascript-application` omits an explicit nested contextBridge member when an inline API object contains an unknown spread. For `{ ...extra, a: { b: 1 }, c: 2 }`, the API observation lists `a` and `c` but loses `a.b`, and `summary.exposed_api_members` is 2 instead of 3. The literal definitions follow the spread, so that spread cannot overwrite them. The no-spread control retains all three members. Both observed CLI commands exited 0 and returned their results under `normalized_result`. ### Reproduction 1. Start from a clean checkout of revision `58300885207133422af235e0f2b10658cf958b03` with the repository's pinned development toolchain. From the repository root, install the locked dependencies and build the CLI: ```sh npm ci npm run build:cached ``` Verification used prepared locked dependencies and built output; it did not rerun these setup commands. 2. Create two inert application inputs and an isolated home for the CLI subprocesses: ```sh mkdir -p repro-work/home repro-work/with-spread repro-work/control cat > repro-work/with-spread/preload.js <<'JS' const { contextBridge } = require("electron"); const extra = getExtra(); contextBridge.exposeInMainWorld("api", { ...extra, a: { b: 1 }, c: 2 }); JS cat > repro-work/control/preload.js <<'JS' const { contextBridge } = require("electron"); contextBridge.exposeInMainWorld("api", { a: { b: 1 }, c: 2 }); JS ``` 3. Analyze each directory with its source file explicitly selected. REA parses the fixture as inert source; it does not execute Electron or `getExtra()`: ```sh repro_home="$PWD/repro-work/home" env HOME="$repro_home" XDG_CONFIG_HOME="$repro_home/config" \ XDG_CACHE_HOME="$repro_home/cache" XDG_DATA_HOME="$repro_home/data" \ node scripts/rea.mjs analyze-javascript-application ./repro-work/with-spread \ --source-path preload.js --format json > repro-work/with-spread.json env HOME="$repro_home" XDG_CONFIG_HOME="$repro_home/config" \ XDG_CACHE_HOME="$repro_home/cache" XDG_DATA_HOME="$repro_home/data" \ node scripts/rea.mjs analyze-javascript-application ./repro-work/control \ --source-path preload.js --format json > repro-work/control.json ``` 4. Print the returned API member lists and counts: ```sh node --input-type=module <<'JS' import fs from "node:fs"; for (const name of ["with-spread", "control"]) { const result = JSON.parse(fs.readFileSync(`repro-work/${name}.json`, "utf8")).normalized_result; console.log(name, "exposed_api_members", result.summary.exposed_api_members); for (const node of result.graph.nodes) { if (node.kind !== "context-bridge-api") continue; for (const observation of node.observations) { console.log(JSON.stringify(observation.properties)); } } } JS ``` ### Expected behavior `docs/javascript-artifact-reconstruction.md:229-230` documents contextBridge API keys and bounded literal member paths. The spread-first input has the same explicit `a.b` path as the control, within the supported depth bound. Its API observation should retain `a`, `a.b`, and `c`, and its summary should count those three retained members. The unresolved spread contribution should remain explicitly unknown. Retaining the later literal members must not imply exact knowledge of additional spread-contributed properties or runtime execution. `AGENTS.md:29` also requires preserving partial facts. ### Impact and scope The successful analysis silently returns an incomplete member list and an undercount for the reproduced input. The API key, top-level members, and spread uncertainty remain available. Verification covered a local extracted application directory and an explicitly selected JavaScript file through the CLI. ASAR and MCP behavior were not replayed. No Electron process or native analysis engine was required, and no security boundary crossing was observed. Removing the spread restores the control output but changes the analyzed artifact. No caller option or alternate supported entry point that preserves the original input and returns the missing graph member was verified. ### REA version 6.4.0 (main at 58300885207133422af235e0f2b10658cf958b03) ### Runtime and platform Node.js v26.10.0; macOS. The verifier did not record the OS version or architecture. ### Target and artifact context Local extracted application directory containing one explicitly selected `preload.js` file. The fixture contains an inline contextBridge API object with an unresolved leading spread followed by explicit literal properties. The control omits the spread. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The observed member fields and summary counts were: ```text with-spread: members=["a","c"], unknown_members=1, exposed_api_members=2 control: members=["a","a.b","c"], unknown_members=0, exposed_api_members=3 ``` The spread observation also returned `api_status: "dynamic"` and `resolution_basis: "wrapper-substitution"`. The control returned `api_status: "object-literal"` and `resolution_basis: "direct-syntax"`. Those fields provide observed context; this report does not require a particular replacement status or provenance implementation. ### Typed outcome or recovery expected Return a successful analysis whose API observation retains `a`, `a.b`, and `c`, whose exposed-member count is 3, and whose unresolved spread remains explicitly uncertain. Keep the no-spread control's three-member list and zero unknown members. Suppressing the API observation, dropping nested paths from the control, or treating the unknown spread as exact would not restore the contract. ### Analysis or suspected cause Source-derived reasoning at the stated baseline explains the observed loss. `src/domain/javascript/electronStaticAnalysisValues.ts:104-167` recursively collects literal dotted member paths while separately counting spread uncertainty. `src/domain/javascript/electronStaticAnalysisBrowser.ts:178-207` first records that literal result. `src/domain/javascript/electronWrappedCallPaths.ts:400-414` constructs a replacement exposure finding using `resolver.members(object)`. The object branch at `src/domain/javascript/javascriptSymbolicBindings.ts:342-354` enumerates top-level keys and spreads without recursively retaining nested literal paths. The replacement at `src/domain/javascript/electronWrappedCallPaths.ts:432-439` substitutes that finding when the original has unknown members. `src/application/javascript/ElectronBoundaryGraphWindows.ts:201` copies the retained member list into the graph, and `src/application/javascript/ElectronBoundaryAnalysis.ts:130-133` counts its length. The incomplete graph member list and summary count are downstream effects of that substitution. These locators explain the failure without prescribing a repair. ### Additional context Related closed issue #2669 concerns missing candidate call paths for nested bridge functions whose member names already remain in the graph; this report concerns a nested literal member missing from the list itself when an unknown spread is present. The supported impact rating is P2 because one bounded API observation and its summary count are incomplete while analysis succeeds and retains other facts. A practical workaround that preserves the original artifact and returned member list remains unverified.
bsakaguchi 2026-10-15 18:31:31 +00:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

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

I reproduced the missing nested member with the production static analyzer on current main, including the no-spread control. I am working on a focused fix and regression coverage. The Windows CLI replay is currently blocked by the native addon capability mismatch, so I am not claiming CLI verification yet.

<!-- gh-comment-id:6161744228 --> @jpdefo commented on GitHub (Oct 14, 2026): I reproduced the missing nested member with the production static analyzer on current main, including the no-spread control. I am working on a focused fix and regression coverage. The Windows CLI replay is currently blocked by the native addon capability mismatch, so I am not claiming CLI verification yet.
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#1149
No description provided.