From 57f30029b14a3035a0141c9249d63d2ee5e9018a Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Wed, 14 Oct 2026 23:25:18 -0700 Subject: [PATCH] fix(javascript): reject Webpack getters replaced through any identity write (#3860) The canonical `n.n` getter proof scanned for direct `loader.n =` assignments and four Object methods on its own. `Reflect.set`, `Reflect.defineProperty`, `delete`, destructuring and unknown-key writes left the proof intact, so a replaced or deleted getter still attributed BrowserWindow calls to Electron. Any Object method call on the loader also rejected the proof, even for an unrelated key. The proof now uses the shared identity-write owner (`collectJavaScriptIdentityWritesSteps` with the module interop's intrinsic resolution), as ambient writes already do. A write whose path may select `n` (the key itself, an unknown key, or the whole loader) counts; exactly one canonical assignment installed before entry dispatch remains required. Fixes #3315 Co-authored-by: Claude Opus 5.5 --- .../electronBundledRequireIdentity.test.ts | 32 +++++++ .../javascriptBundledRequireOrigin.ts | 94 ++++++++----------- 2 files changed, 72 insertions(+), 54 deletions(-) diff --git a/src/domain/javascript/electronBundledRequireIdentity.test.ts b/src/domain/javascript/electronBundledRequireIdentity.test.ts index 1904daef9e..ff1e187fa8 100644 --- a/src/domain/javascript/electronBundledRequireIdentity.test.ts +++ b/src/domain/javascript/electronBundledRequireIdentity.test.ts @@ -48,6 +48,38 @@ describe("Electron identity through Webpack externals", () => { expect(electronOf(source).browser_windows).toEqual([]); }); + const GETTER_CONSUMER = "const s=n.n(n(45840));new (s().BrowserWindow)({});"; + it.each([ + ["Reflect.set", 'Reflect.set(n,"n",e=>()=>({BrowserWindow:class{}}));'], + [ + "Reflect.defineProperty", + 'Reflect.defineProperty(n,"n",{value:e=>()=>0});', + ], + ["delete", "delete n.n;"], + ["a destructuring write", "({g:n.n}={g:e=>()=>0});"], + ["an unknown key", "n[key]=e=>()=>0;"], + ])( + "does not infer Electron after a getter write through %s", + (_name, write) => { + expect( + electronOf(bootstrap(EXTERNAL, GETTER_CONSUMER, `${write}n(100);`)) + .browser_windows, + ).toEqual([]); + }, + ); + + it("keeps the getter after a reflective write to another loader key", () => { + expect( + electronOf( + bootstrap( + EXTERNAL, + GETTER_CONSUMER, + 'Object.defineProperty(n,"d",{value:1});n(100);', + ), + ).browser_windows, + ).toHaveLength(1); + }); + it("follows the loader passed to a module factory", () => { const electron = electronOf(bootstrap(EXTERNAL, CONSUMER)); expect(electron.browser_windows).toMatchObject([ diff --git a/src/domain/javascript/javascriptBundledRequireOrigin.ts b/src/domain/javascript/javascriptBundledRequireOrigin.ts index f6e5749f70..dbfe873e08 100644 --- a/src/domain/javascript/javascriptBundledRequireOrigin.ts +++ b/src/domain/javascript/javascriptBundledRequireOrigin.ts @@ -2,7 +2,12 @@ import * as t from "@babel/types"; import { javascriptBootstrapAt } from "./javascriptBootstrap.js"; import { proveJavaScriptBootstrapIntegrity } from "./javascriptBootstrapIntegrity.js"; -import { semanticStaticPropertyKey } from "./javascriptAstValues.js"; +import { + semanticStaticPropertyKey, + unwrapJavaScriptExpression, +} from "./javascriptAstValues.js"; +import { collectJavaScriptIdentityWritesSteps } from "./javascriptElectronMemberWrites.js"; +import { createSemanticModuleInterop } from "./javascriptModuleInterop.js"; import { semanticMemberChain, semanticRequireOrigin, @@ -18,7 +23,10 @@ import { moduleFactory, modulePropertyName, } from "./javascriptStaticAnalysisHelpers.js"; -import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; +import { + completeSemanticSteps, + traverseJavaScriptAst, +} from "./javascriptSemanticTraversal.js"; /** Loader bindings and literal externals proven for one standalone bootstrap. */ interface BootstrapExternals { @@ -142,60 +150,38 @@ const canonicalWebpackInteropBindings = ( bootstrapBody: t.BlockStatement, state: JavaScriptSemanticAnalysisState, ): ReadonlySet => { - const writes = new Map(); - let reflectiveWrite = false; - traverseJavaScriptAst(state.program, { - enter: (node) => { - if (t.isAssignmentExpression(node) && t.isMemberExpression(node.left)) { - const target = node.left; - const bindingId = t.isIdentifier(target.object) - ? resolveSemanticBindingState( - state, - target.object, - target.object.name, - )?.bindingId - : undefined; - if ( - semanticStaticPropertyKey(target.property, target.computed) === "n" && - bindingId !== undefined && - loaderBindingIds.has(bindingId) - ) - writes.set(bindingId, [...(writes.get(bindingId) ?? []), node]); - } - if ( - !t.isCallExpression(node) || - !t.isMemberExpression(node.callee) || - !t.isIdentifier(node.callee.object, { name: "Object" }) || - ![ - "defineProperty", - "defineProperties", - "assign", - "setPrototypeOf", - ].includes( - semanticStaticPropertyKey( - node.callee.property, - node.callee.computed, - ) ?? "", - ) || - !t.isIdentifier(node.arguments[0]) - ) - return; - reflectiveWrite ||= loaderBindingIds.has( - resolveSemanticBindingState( - state, - node.arguments[0], - node.arguments[0].name, - )?.bindingId ?? "", - ); - }, - }); + // The shared identity-write owner covers assignment, delete, destructuring, + // and Object/Reflect mutators; any write that may select `n` counts. + const writes = new Set(); + const interop = createSemanticModuleInterop(state); + completeSemanticSteps( + collectJavaScriptIdentityWritesSteps( + state.program, + (root, kind) => { + const node = unwrapJavaScriptExpression(root).node; + return kind === "member" && + t.isIdentifier(node) && + loaderBindingIds.has( + resolveSemanticBindingState(state, node, node.name)?.bindingId ?? + "", + ) + ? [["loader"]] + : []; + }, + interop.intrinsicInvocation, + interop.staticString, + interop.isFreshObject, + (node, path) => { + if (path.length < 2 || path[1] === "n" || path[1] === null) + writes.add(node); + }, + ), + ); const proven = new Set(); - if (reflectiveWrite) return proven; - const allWrites = [...writes.values()].flat(); - const [write] = allWrites; + const [write] = writes; if ( - allWrites.length === 1 && - write !== undefined && + writes.size === 1 && + t.isAssignmentExpression(write) && isInstalledBeforeEntryDispatch( write, bootstrapBody,