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 <noreply@anthropic.com>
This commit is contained in:
1 parent
22ec8bdb2f
commit
57f30029b1
2 files changed
+72
-54
No files matched your search
@@ -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([
|
||||
|
||||
@@ -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<string> => {
|
||||
const writes = new Map<string, t.AssignmentExpression[]>();
|
||||
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<t.Node>();
|
||||
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<string>();
|
||||
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,
|
||||
|
||||
Reference in new issue
Block a user