[GH-ISSUE #6544] [Bug] Local objects named localStorage produce exact browser-storage evidence #3935

Closed
opened 2026-10-31 00:45:22 +00:00 by bsakaguchi · 2 comments
Owner

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

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

A lexical const localStorage initialized to an ordinary object with a return-only setItem method produces a storage graph node with storage_kind: "local-storage" and a persists_to edge. Its evidence is marked state: "observed", confidence: "exact", with no limitations. The graph reports complete coverage despite no reference to the browser storage API. This was observed by executing production source analysis and graph-building functions on the published release and main; no CLI/MCP execution is claimed.

Reproduction

The executed probe imported analyzeJavaScriptArtifactFiles and buildJavaScriptArtifactGraph from the selected checkout, passed retained file records with real SHA-256 digests and UTF-8 byte lengths, and supplied a minimal in-memory directory inventory snapshot. It exercised the production parser, analyzer, and validated application graph builder. It did not exercise filesystem acquisition, the compiled worker, CLI, or MCP transport, and it never evaluated the fixture JavaScript. The fixture's container hash was SHA-256 of the text fixture; there were no unavailable files, nested containers, or integrity contradictions.

The following equivalent focused probe shows the complete input construction. Its literal reduced presentation was not separately executed; the executed probe contained the same case alongside other independent fixtures. Save it as probe.mts outside the checkout, select the checkout as the working directory, and run node --import tsx /path/to/probe.mts "$PWD" after installing the checkout's locked dependencies.

import { createHash } from "node:crypto";
import { pathToFileURL } from "node:url";
const repo = process.argv[2];
const load = (p: string) => import(pathToFileURL(`${repo}/src/${p}.ts`).href);
const { analyzeJavaScriptArtifactFiles } = await load("application/javascript/JavaScriptArtifactAnalysis");
const { buildJavaScriptArtifactGraph } = await load("application/javascript/JavaScriptArtifactGraphBuilder");
const hash = (s: string) => createHash("sha256").update(s).digest("hex");
const rootHash = hash("fixture");
const entries = { "main.js": 'const localStorage = { setItem(key, value) { return value; } }; localStorage.setItem("cache-key", "value");' };
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: path.endsWith(".js") ? "javascript" : "json",
  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 graph = buildJavaScriptArtifactGraph(snapshot, fileSet, analyzeJavaScriptArtifactFiles(fileSet));
console.log(JSON.stringify({ nodes: graph.nodes.filter((n: any) => n.kind === "storage"), edges: graph.edges.filter((e: any) => ["persists_to"].includes(e.relation)), coverage: graph.coverage }));

Both selected checkouts produced the result described below in the initial probe pass. This is a focused production-function reproduction, not an end-to-end public-interface reproduction.

Expected behavior

The lexical binding and its locally defined method should prevent this call from being classified as browser local storage. A syntax spelling alone should not become exact observed API evidence when the binding definition proves an ordinary local object. REA already preserves uncertain SQLite constructor identities as candidates in the same storage analysis and distinguishes lexical receivers for other browser APIs.

Impact and scope

Both tested revisions falsely claim that the fixture persists cache-key to browser storage. This changes the application graph and can mislead storage/data-flow analysis. The demonstrated scope is a local binding shadowing localStorage; the similarly named sessionStorage branch is source-visible but was not executed.

REA version

Published rea-agents-6.7.0, source revision 27dc42d10249f0410904a75c0997842eb03b931c; main source revision 1635eadfcc49bbb80178838ab74667c2981de383. Both were probed with their locked dependencies.

Runtime and platform

Node.js v26.12.1; Linux x86_64. No browser, Electron target, Hopper, Ghidra, or external analysis provider was started.

Target and artifact context

One inert JavaScript file, main.js, represented by a retained source record in an in-memory directory inventory. No browser context or persistent storage existed.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The executed probe returned this node observation on both revisions:

{"storage_kind":"local-storage","name":"cache-key","mechanism":"call:localStorage.setItem"}

Its evidence reported authority: "ast-static-analysis", state: "observed", confidence: "exact", and an empty limitations array. The graph included a persists_to edge and coverage.status: "complete".

The published name classifier matches name.includes("localStorage."). The published consumer uses receiver facts for selected .open APIs, then falls back to this spelling check for the fixture. Main retains that consumer and the name classifier. The published graph projection promotes the result to exact AST storage evidence and emits persists_to.

Additional context

Deduplication covered the open issue inventory and open/closed searches for localStorage shadow, storage unrelated, local-storage static, and storage shadowed. No matching report was found. Issue #6289 concerns captured storage string digests, which is a different runtime-capture path.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Valid ordinary source produces a false application graph fact with complete coverage on both the latest published release and main. The demonstrated impact is bounded to the affected classification, with no data mutation or execution.
  • Workaround status: unverified
  • Workaround basis: An analyst can recognize this reduced fixture by reading its local receiver definition, but no general workflow that filters only false nodes while retaining genuine findings was tested.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6544 ### 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 A lexical `const localStorage` initialized to an ordinary object with a return-only `setItem` method produces a `storage` graph node with `storage_kind: "local-storage"` and a `persists_to` edge. Its evidence is marked `state: "observed"`, `confidence: "exact"`, with no limitations. The graph reports complete coverage despite no reference to the browser storage API. This was observed by executing production source analysis and graph-building functions on the published release and main; no CLI/MCP execution is claimed. ### Reproduction The executed probe imported `analyzeJavaScriptArtifactFiles` and `buildJavaScriptArtifactGraph` from the selected checkout, passed retained file records with real SHA-256 digests and UTF-8 byte lengths, and supplied a minimal in-memory directory inventory snapshot. It exercised the production parser, analyzer, and validated application graph builder. It did not exercise filesystem acquisition, the compiled worker, CLI, or MCP transport, and it never evaluated the fixture JavaScript. The fixture's container hash was SHA-256 of the text `fixture`; there were no unavailable files, nested containers, or integrity contradictions. The following equivalent focused probe shows the complete input construction. Its literal reduced presentation was not separately executed; the executed probe contained the same case alongside other independent fixtures. Save it as `probe.mts` outside the checkout, select the checkout as the working directory, and run `node --import tsx /path/to/probe.mts "$PWD"` after installing the checkout's locked dependencies. ```typescript import { createHash } from "node:crypto"; import { pathToFileURL } from "node:url"; const repo = process.argv[2]; const load = (p: string) => import(pathToFileURL(`${repo}/src/${p}.ts`).href); const { analyzeJavaScriptArtifactFiles } = await load("application/javascript/JavaScriptArtifactAnalysis"); const { buildJavaScriptArtifactGraph } = await load("application/javascript/JavaScriptArtifactGraphBuilder"); const hash = (s: string) => createHash("sha256").update(s).digest("hex"); const rootHash = hash("fixture"); const entries = { "main.js": 'const localStorage = { setItem(key, value) { return value; } }; localStorage.setItem("cache-key", "value");' }; 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: path.endsWith(".js") ? "javascript" : "json", 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 graph = buildJavaScriptArtifactGraph(snapshot, fileSet, analyzeJavaScriptArtifactFiles(fileSet)); console.log(JSON.stringify({ nodes: graph.nodes.filter((n: any) => n.kind === "storage"), edges: graph.edges.filter((e: any) => ["persists_to"].includes(e.relation)), coverage: graph.coverage })); ``` Both selected checkouts produced the result described below in the initial probe pass. This is a focused production-function reproduction, not an end-to-end public-interface reproduction. ### Expected behavior The lexical binding and its locally defined method should prevent this call from being classified as browser local storage. A syntax spelling alone should not become exact observed API evidence when the binding definition proves an ordinary local object. REA already preserves uncertain SQLite constructor identities as candidates in the same storage analysis and distinguishes lexical receivers for other browser APIs. ### Impact and scope Both tested revisions falsely claim that the fixture persists `cache-key` to browser storage. This changes the application graph and can mislead storage/data-flow analysis. The demonstrated scope is a local binding shadowing `localStorage`; the similarly named `sessionStorage` branch is source-visible but was not executed. ### REA version Published `rea-agents-6.7.0`, source revision `27dc42d10249f0410904a75c0997842eb03b931c`; main source revision `1635eadfcc49bbb80178838ab74667c2981de383`. Both were probed with their locked dependencies. ### Runtime and platform Node.js v26.12.1; Linux x86_64. No browser, Electron target, Hopper, Ghidra, or external analysis provider was started. ### Target and artifact context One inert JavaScript file, `main.js`, represented by a retained source record in an in-memory directory inventory. No browser context or persistent storage existed. ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The executed probe returned this node observation on both revisions: ```json {"storage_kind":"local-storage","name":"cache-key","mechanism":"call:localStorage.setItem"} ``` Its evidence reported `authority: "ast-static-analysis"`, `state: "observed"`, `confidence: "exact"`, and an empty limitations array. The graph included a `persists_to` edge and `coverage.status: "complete"`. The [published name classifier](https://github.com/morluto/rea/blob/27dc42d10249f0410904a75c0997842eb03b931c/src/domain/javascript/javascriptStaticAnalysisHelpers.ts#L674) matches `name.includes("localStorage.")`. The [published consumer](https://github.com/morluto/rea/blob/27dc42d10249f0410904a75c0997842eb03b931c/src/domain/javascript/javascriptStaticAnalysisCalls.ts#L780) uses receiver facts for selected `.open` APIs, then falls back to this spelling check for the fixture. [Main retains that consumer](https://github.com/morluto/rea/blob/1635eadfcc49bbb80178838ab74667c2981de383/src/domain/javascript/javascriptStaticAnalysisCalls.ts#L799) and the [name classifier](https://github.com/morluto/rea/blob/1635eadfcc49bbb80178838ab74667c2981de383/src/domain/javascript/javascriptStaticAnalysisHelpers.ts#L689). The [published graph projection](https://github.com/morluto/rea/blob/27dc42d10249f0410904a75c0997842eb03b931c/src/application/javascript/JavaScriptArtifactGraphFindings.ts#L291) promotes the result to exact AST storage evidence and emits `persists_to`. ### Additional context Deduplication covered the open issue inventory and open/closed searches for `localStorage shadow`, `storage unrelated`, `local-storage static`, and `storage shadowed`. No matching report was found. Issue [#6289](https://github.com/morluto/rea/issues/6289) concerns captured storage string digests, which is a different runtime-capture path. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Valid ordinary source produces a false application graph fact with complete coverage on both the latest published release and main. The demonstrated impact is bounded to the affected classification, with no data mutation or execution. - Workaround status: unverified - Workaround basis: An analyst can recognize this reduced fixture by reading its local receiver definition, but no general workflow that filters only false nodes while retaining genuine findings was tested.
Author
Owner

@Jackzigen commented on GitHub (Oct 24, 2026):

Verified with the probe from the issue report.

  • At the reported revision 1635eadf the probe still produces the phantom storage node and persists_to edge for the local localStorage object.
  • Re-running the same probe at current main produces neither.
  • The fix lands at 5f13ddd75 (fix(javascript): require API identity for static application roles): the probe reproduces at its parent f91b7f800 and is clean from 5f13ddd75 onward.

Looks resolved — this can be closed.

<!-- gh-comment-id:6380427950 --> @Jackzigen commented on GitHub (Oct 24, 2026): Verified with the probe from the issue report. - At the reported revision `1635eadf` the probe still produces the phantom `storage` node and `persists_to` edge for the local `localStorage` object. - Re-running the same probe at current `main` produces neither. - The fix lands at `5f13ddd75` (*fix(javascript): require API identity for static application roles*): the probe reproduces at its parent `f91b7f800` and is clean from `5f13ddd75` onward. Looks resolved — this can be closed.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

Browser storage classification now requires an unmodified global API reference; the lexical ordinary object in the report cannot pass that identity guard. A production-source probe ran the exact return-only localStorage fixture and a sessionStorage sibling, obtaining no storage facts, while globalThis controls produced the expected fact. The domain regression asserts that result, and the acceptance test asserts the final graph contains only the real storage node.

Implementation: javascriptStaticAnalysisCalls.ts, javascriptStaticAnalysisCalls.ts.

The relevant regression checks in javascriptStaticApiIdentity.test.ts, javascriptStaticApiIdentity.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380724718 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Browser storage classification now requires an unmodified global API reference; the lexical ordinary object in the report cannot pass that identity guard. A production-source probe ran the exact return-only localStorage fixture and a sessionStorage sibling, obtaining no storage facts, while globalThis controls produced the expected fact. The domain regression asserts that result, and the acceptance test asserts the final graph contains only the real storage node. Implementation: [javascriptStaticAnalysisCalls.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/javascript/javascriptStaticAnalysisCalls.ts#L817), [javascriptStaticAnalysisCalls.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/javascript/javascriptStaticAnalysisCalls.ts#L868). The relevant regression checks in [javascriptStaticApiIdentity.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/javascript/javascriptStaticApiIdentity.test.ts), [javascriptStaticApiIdentity.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/acceptance/applications/javascriptStaticApiIdentity.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
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#3935
No description provided.