[GH-ISSUE #6575] [Bug] A local URL constructor creates a false resolved Electron preload #3964

Closed
opened 2026-10-31 00:45:38 +00:00 by bsakaguchi · 0 comments
Owner

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

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

On current main, a local class named URL overrides the requested filename and returns a native URL for actual.js. REA instead reports preload_path: "./decoy.js", resolves an electron-preload node to decoy.js, and adds high-confidence loads relationships with complete graph coverage. Removing the local class produces the same REA path, although it changes the expression's actual value.

This was executed through the production source analyzer and validated application graph builder. A separate Node control executed only the harmless URL expression and returned /fixture/actual.js. No Electron application, CLI, MCP request, or fixture module was launched.

Reproduction

From the audited checkout with its locked dependencies installed, save the following as preload-repro.mts in a disposable directory, then run:

./node_modules/.bin/tsx "$WORK/preload-repro.mts" "$PWD"

WORK identifies that disposable directory. This is the actual portable probe that ran, including its shadowed-constructor and native-constructor cases. The in-memory records use content digests and a minimal directory inventory; filesystem acquisition and the compiled worker are outside this reproduction.

import { createHash } from 'node:crypto';
import { fileURLToPath, pathToFileURL } from 'node:url';
const repo = process.argv[2];
const load = (p) => import(pathToFileURL(`${repo}/src/${p}.ts`).href);
const { analyzeJavaScriptArtifactFiles } = await load('application/javascript/JavaScriptArtifactAnalysis');
const { buildJavaScriptArtifactGraph } = await load('application/javascript/JavaScriptArtifactGraphBuilder');
const hash = (s) => createHash('sha256').update(s).digest('hex');
const rootHash = hash('url-shadow-fixture');
const local = `class URL { constructor(_input, base) { return new globalThis.URL('./actual.js', base); } }`;
for (const shadowed of [true, false]) {
  const source = `import {BrowserWindow} from 'electron'; import {fileURLToPath} from 'node:url'; ${shadowed ? local : ''} new BrowserWindow({webPreferences:{preload:fileURLToPath(new URL('./decoy.js',import.meta.url))}});`;
  const entries = {'main.mjs':source, 'decoy.js':'export const decoy=1;', 'actual.js':'export const actual=1;'};
  const files = Object.entries(entries).map(([path,text]) => ({
    path, container_sha256:rootHash, sha256:hash(text), bytes:Buffer.byteLength(text),
    inventory_artifact_id:`art_${hash(text)}`, kind:'javascript', unpacked:false,
    text:{included:true,value:text},
  }));
  const fileSet = {files, unavailable_relevant_files:[], containers:[], text_bytes_read:files.reduce((n,f)=>n+f.bytes,0), invalid_utf8_files:0};
  const snapshot = {
    manifest:{root_artifact_id:`art_${rootHash}`,root_sha256:rootHash,root_format:'directory',manifest_id:`agm_${rootHash}`,graph_sha256:rootHash},
    nodes:[{artifact_id:`art_${rootHash}`,sha256:rootHash,size:1}], occurrences:[], integrity_contradictions:[],
  };
  const analysis = analyzeJavaScriptArtifactFiles(fileSet);
  const graph = buildJavaScriptArtifactGraph(snapshot,fileSet,analysis);
  console.log(JSON.stringify({shadowed,parse:analysis.files[0].javascript.parse_status,
    preload:analysis.files[0].javascript.electron.browser_windows[0].preload_path,
    preloads:graph.nodes.filter(n=>n.kind==='electron-preload').flatMap(n=>n.observations.map(o=>o.properties)),
    loads:graph.edges.filter(e=>e.relation==='loads').map(e=>({properties:e.properties,confidence:e.evidence.confidence})),coverage:graph.coverage}));
}
const actual = Function('fileURLToPath',`${local}; return fileURLToPath(new URL('./decoy.js','file:///fixture/main.mjs'));`)(fileURLToPath);
console.log(JSON.stringify({node_control:actual}));

Both REA cases returned parse: "complete", preload: "./decoy.js", and resolved preload observations for decoy.js. The final Node control returned {"node_control":"/fixture/actual.js"}. The portable probe ran once before drafting; an earlier development probe also observed the same constructor case.

Expected behavior

A lexically replaced URL constructor must not be interpreted as the global URL constructor. Either retain the preload expression as dynamic with no resolved decoy-file relationship, or recover the actual local implementation's result with appropriate evidence. The Node control establishes the value for this exact input; REA does not need to execute target code to avoid the false built-in attribution.

The repository's architecture contract requires preserving identity and uncertainty at interpretation boundaries. Its worker URL extractor already requires an unmodified global URL before interpreting the same constructor syntax.

Impact and scope

A valid ESM source with a local URL class produces a wrong resolved preload architecture relationship and may direct an analyst to an unrelated existing file. The observed failure is bounded static evidence corruption; no target mutation, execution, outage, or security effect was observed. Only the included constructor replacement and native control were compared here. NOT_ENOUGH_INFO: prevalence in real applications and a practical result-preserving REA workaround.

REA version

Executed current main 46111dd091f2ae01baab9eb119b53b9bd309a05b; package metadata says 6.7.0. Runtime reproduction is main-only. The latest published release, rea-agents-6.7.0 at 27dc42d10249f0410904a75c0997842eb03b931c, was not executed. Its constructor-name check has the same missing identity guard; release applicability beyond that source observation remains unverified. This report does not claim an executed failure in the published package.

Runtime and platform

Node.js v24.22.1; Linux x86_64; the checkout's locked dependencies and tsx. No browser, Electron runtime, Hopper, Ghidra, or other provider ran.

Target and artifact context

Self-authored inert ESM application records: main.mjs, actual.js, and decoy.js. No ASAR, native binary, real client configuration, or private target data.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed shadowed-constructor output, reduced from the production graph result:

{"parse":"complete","preload":"./decoy.js","preloads":[{"declared_path":"./decoy.js","resolution_context":"filesystem-expression","resolved_path":"decoy.js","resolution_status":"resolved","limitations":[],"mechanism":"BrowserWindow:webPreferences.preload"}],"coverage":{"status":"complete","truncated":false,"omitted_count":0,"limits":[]}}

The corresponding BrowserWindow loads edge has confidence: "high" and resolved_path: "decoy.js". A second preload observation uses mechanism: "property:preload". Node's executed control returned /fixture/actual.js, not the decoy path. Source analysis and graph projection ran; public-interface execution and published-package runtime behavior were not tested.

Primary source references:

Typed outcome or recovery expected

Keep the local-constructor preload unresolved unless its returned URL is actually established. In particular, do not emit a resolved loads edge to decoy.js for the shadowed case. Retain genuine global-constructor recovery in the control.

Analysis or suspected cause

Source-derived: the preload path reducer recognizes the constructor by its name alone, then applies native URL behavior to its first literal argument. It does not establish that the local class implements that behavior. This is a different producer branch from the unrelated join/resolve calls in #3962.

Additional context

Open-issue titles and bodies were searched locally. All-state GitHub searches used "fileURLToPath", "URL" "shadow", and "preload" "URL". No matching constructor-replacement report was found. Closed #2561 concerns genuine imported file-URL conversion and explicitly excludes shadowing; its source-relative control does not establish constructor identity. Parent final deduplication remains separate.

No AutoReview, independent review, or re-verification was performed after drafting.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The production analyzer and graph builder return a wrong resolved preload path with high-confidence edges for a bounded valid source input; a Node control and the native-constructor comparison establish the mismatch.
  • Workaround status: unverified
  • Workaround basis: Reading the local class explains the reduced fixture, but no REA workflow preserving the original target and correcting its graph was demonstrated.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6575 ## 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 On current main, a local class named `URL` overrides the requested filename and returns a native URL for `actual.js`. REA instead reports `preload_path: "./decoy.js"`, resolves an `electron-preload` node to `decoy.js`, and adds high-confidence `loads` relationships with complete graph coverage. Removing the local class produces the same REA path, although it changes the expression's actual value. This was executed through the production source analyzer and validated application graph builder. A separate Node control executed only the harmless URL expression and returned `/fixture/actual.js`. No Electron application, CLI, MCP request, or fixture module was launched. ## Reproduction From the audited checkout with its locked dependencies installed, save the following as `preload-repro.mts` in a disposable directory, then run: ```sh ./node_modules/.bin/tsx "$WORK/preload-repro.mts" "$PWD" ``` `WORK` identifies that disposable directory. This is the actual portable probe that ran, including its shadowed-constructor and native-constructor cases. The in-memory records use content digests and a minimal directory inventory; filesystem acquisition and the compiled worker are outside this reproduction. ```typescript import { createHash } from 'node:crypto'; import { fileURLToPath, pathToFileURL } from 'node:url'; const repo = process.argv[2]; const load = (p) => import(pathToFileURL(`${repo}/src/${p}.ts`).href); const { analyzeJavaScriptArtifactFiles } = await load('application/javascript/JavaScriptArtifactAnalysis'); const { buildJavaScriptArtifactGraph } = await load('application/javascript/JavaScriptArtifactGraphBuilder'); const hash = (s) => createHash('sha256').update(s).digest('hex'); const rootHash = hash('url-shadow-fixture'); const local = `class URL { constructor(_input, base) { return new globalThis.URL('./actual.js', base); } }`; for (const shadowed of [true, false]) { const source = `import {BrowserWindow} from 'electron'; import {fileURLToPath} from 'node:url'; ${shadowed ? local : ''} new BrowserWindow({webPreferences:{preload:fileURLToPath(new URL('./decoy.js',import.meta.url))}});`; const entries = {'main.mjs':source, 'decoy.js':'export const decoy=1;', 'actual.js':'export const actual=1;'}; const files = Object.entries(entries).map(([path,text]) => ({ path, container_sha256:rootHash, sha256:hash(text), bytes:Buffer.byteLength(text), inventory_artifact_id:`art_${hash(text)}`, kind:'javascript', unpacked:false, text:{included:true,value:text}, })); const fileSet = {files, unavailable_relevant_files:[], containers:[], text_bytes_read:files.reduce((n,f)=>n+f.bytes,0), invalid_utf8_files:0}; const snapshot = { manifest:{root_artifact_id:`art_${rootHash}`,root_sha256:rootHash,root_format:'directory',manifest_id:`agm_${rootHash}`,graph_sha256:rootHash}, nodes:[{artifact_id:`art_${rootHash}`,sha256:rootHash,size:1}], occurrences:[], integrity_contradictions:[], }; const analysis = analyzeJavaScriptArtifactFiles(fileSet); const graph = buildJavaScriptArtifactGraph(snapshot,fileSet,analysis); console.log(JSON.stringify({shadowed,parse:analysis.files[0].javascript.parse_status, preload:analysis.files[0].javascript.electron.browser_windows[0].preload_path, preloads:graph.nodes.filter(n=>n.kind==='electron-preload').flatMap(n=>n.observations.map(o=>o.properties)), loads:graph.edges.filter(e=>e.relation==='loads').map(e=>({properties:e.properties,confidence:e.evidence.confidence})),coverage:graph.coverage})); } const actual = Function('fileURLToPath',`${local}; return fileURLToPath(new URL('./decoy.js','file:///fixture/main.mjs'));`)(fileURLToPath); console.log(JSON.stringify({node_control:actual})); ``` Both REA cases returned `parse: "complete"`, `preload: "./decoy.js"`, and resolved preload observations for `decoy.js`. The final Node control returned `{"node_control":"/fixture/actual.js"}`. The portable probe ran once before drafting; an earlier development probe also observed the same constructor case. ## Expected behavior A lexically replaced `URL` constructor must not be interpreted as the global URL constructor. Either retain the preload expression as dynamic with no resolved decoy-file relationship, or recover the actual local implementation's result with appropriate evidence. The Node control establishes the value for this exact input; REA does not need to execute target code to avoid the false built-in attribution. The repository's [architecture contract](https://github.com/morluto/rea/blob/46111dd091f2ae01baab9eb119b53b9bd309a05b/AGENTS.md#architecture-and-cleanup) requires preserving identity and uncertainty at interpretation boundaries. Its [worker URL extractor](https://github.com/morluto/rea/blob/46111dd091f2ae01baab9eb119b53b9bd309a05b/src/domain/javascript/javascriptStaticAnalysisCalls.ts#L188-L204) already requires an unmodified global `URL` before interpreting the same constructor syntax. ## Impact and scope A valid ESM source with a local `URL` class produces a wrong resolved preload architecture relationship and may direct an analyst to an unrelated existing file. The observed failure is bounded static evidence corruption; no target mutation, execution, outage, or security effect was observed. Only the included constructor replacement and native control were compared here. NOT_ENOUGH_INFO: prevalence in real applications and a practical result-preserving REA workaround. ## REA version Executed current main `46111dd091f2ae01baab9eb119b53b9bd309a05b`; package metadata says `6.7.0`. Runtime reproduction is main-only. The latest published release, `rea-agents-6.7.0` at `27dc42d10249f0410904a75c0997842eb03b931c`, was not executed. Its [constructor-name check](https://github.com/morluto/rea/blob/27dc42d10249f0410904a75c0997842eb03b931c/src/domain/javascript/javascriptStaticAnalysisHelpers.ts#L317-L323) has the same missing identity guard; release applicability beyond that source observation remains unverified. This report does not claim an executed failure in the published package. ## Runtime and platform Node.js v24.22.1; Linux x86_64; the checkout's locked dependencies and `tsx`. No browser, Electron runtime, Hopper, Ghidra, or other provider ran. ## Target and artifact context Self-authored inert ESM application records: `main.mjs`, `actual.js`, and `decoy.js`. No ASAR, native binary, real client configuration, or private target data. ## Interface Other ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence Observed shadowed-constructor output, reduced from the production graph result: ```json {"parse":"complete","preload":"./decoy.js","preloads":[{"declared_path":"./decoy.js","resolution_context":"filesystem-expression","resolved_path":"decoy.js","resolution_status":"resolved","limitations":[],"mechanism":"BrowserWindow:webPreferences.preload"}],"coverage":{"status":"complete","truncated":false,"omitted_count":0,"limits":[]}} ``` The corresponding BrowserWindow `loads` edge has `confidence: "high"` and `resolved_path: "decoy.js"`. A second preload observation uses `mechanism: "property:preload"`. Node's executed control returned `/fixture/actual.js`, not the decoy path. Source analysis and graph projection ran; public-interface execution and published-package runtime behavior were not tested. Primary source references: - [Static conversion path](https://github.com/morluto/rea/blob/46111dd091f2ae01baab9eb119b53b9bd309a05b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts#L258-L267) calls `isSourceFileUrl` before decoding the source-relative expression. - [Constructor recognition](https://github.com/morluto/rea/blob/46111dd091f2ae01baab9eb119b53b9bd309a05b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts#L328-L334) accepts the callee spelling `URL` or `.URL` without consulting its binding or mutations. - [Preload producer](https://github.com/morluto/rea/blob/46111dd091f2ae01baab9eb119b53b9bd309a05b/src/domain/javascript/electronStaticAnalysisBrowser.ts#L198-L212) adopts that derived path. - [Graph projection](https://github.com/morluto/rea/blob/46111dd091f2ae01baab9eb119b53b9bd309a05b/src/application/javascript/ElectronBoundaryGraphWindows.ts#L146-L194) resolves the derived path and emits the BrowserWindow relationship. ## Typed outcome or recovery expected Keep the local-constructor preload unresolved unless its returned URL is actually established. In particular, do not emit a resolved `loads` edge to `decoy.js` for the shadowed case. Retain genuine global-constructor recovery in the control. ## Analysis or suspected cause Source-derived: the preload path reducer recognizes the constructor by its name alone, then applies native URL behavior to its first literal argument. It does not establish that the local class implements that behavior. This is a different producer branch from the unrelated `join`/`resolve` calls in [#3962](https://github.com/morluto/rea/issues/3962). ## Additional context Open-issue titles and bodies were searched locally. All-state GitHub searches used `"fileURLToPath"`, `"URL" "shadow"`, and `"preload" "URL"`. No matching constructor-replacement report was found. Closed [#2561](https://github.com/morluto/rea/issues/2561) concerns genuine imported file-URL conversion and explicitly excludes shadowing; its source-relative control does not establish constructor identity. Parent final deduplication remains separate. No AutoReview, independent review, or re-verification was performed after drafting. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The production analyzer and graph builder return a wrong resolved preload path with high-confidence edges for a bounded valid source input; a Node control and the native-constructor comparison establish the mismatch. - Workaround status: unverified - Workaround basis: Reading the local class explains the reduced fixture, but no REA workflow preserving the original target and correcting its graph was demonstrated.
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#3964
No description provided.