fix(electron): select nested handlers by source coordinates

This commit is contained in:
morluto committed 2026-10-27 11:30:37 +08:00
1 parent 5d5fef7391
commit 319fee4691
2 files changed
+60 -24

No files matched your search

@@ -1,4 +1,5 @@
import {
compareJavaScriptSourcePoints,
javascriptSourceRangeKey,
javascriptSourceRangeContains,
} from "../../domain/javascript/javascriptSourceCoordinates.js";
@@ -381,31 +382,29 @@ const containingHandler = (
records: readonly ElectronIpcRecord[],
file: JavaScriptArtifactFile,
validation: ElectronSenderValidationFinding,
): ElectronIpcRecord | undefined =>
records
.filter(
(record) =>
record.file.path === file.path &&
record.finding.handler_location !== null &&
javascriptSourceRangeContains(
record.finding.handler_location,
validation.location,
),
): ElectronIpcRecord | undefined => {
let selected: ElectronIpcRecord | undefined;
for (const record of records) {
const location = record.finding.handler_location;
if (
record.file.path !== file.path ||
location === null ||
!javascriptSourceRangeContains(location, validation.location)
)
.sort(
(left, right) =>
rangeMagnitude(left.finding.handler_location) -
rangeMagnitude(right.finding.handler_location),
)[0];
const rangeMagnitude = (
range: ElectronIpcRecord["finding"]["handler_location"],
): number =>
range === null
? Number.MAX_SAFE_INTEGER
: (range.end.line - range.start.line) * 1_000_000 +
range.end.column -
range.start.column;
continue;
const previous = selected?.finding.handler_location;
// Parsed handler ranges nest: the innermost starts latest, then ends
// earliest. Columns have no fixed upper bound in minified JavaScript.
if (
previous == null ||
compareJavaScriptSourcePoints(location.start, previous.start) > 0 ||
(compareJavaScriptSourcePoints(location.start, previous.start) === 0 &&
compareJavaScriptSourcePoints(location.end, previous.end) < 0)
)
selected = record;
}
return selected;
};
const coverageFor = (
context: JavaScriptArtifactGraphContext,
@@ -1,3 +1,5 @@
import { addElectronIpcBoundaries } from "./ElectronBoundaryGraphIpc.js";
import { createElectronIpcContext } from "./ElectronBoundaryAnalysis.js";
import { describe, expect, it } from "vitest";
import { createHash } from "node:crypto";
@@ -443,3 +445,38 @@ it.each([
}
},
);
it("attaches sender validation to the innermost handler after a long minified line", () => {
const source = `const {ipcMain}=require('electron');/*${"x".repeat(2_000_010)}*/ipcMain.handle('outer', event=>{
ipcMain.handle('inner', event=>{ if(event.senderFrame.url === 'https://trusted.test') return true; });
});`;
const file = {
...skippedFile(),
bytes: Buffer.byteLength(source),
text: { included: true as const, value: source },
};
const javascript = analyzeJavaScriptStaticSource(source);
expect(javascript.electron.sender_validations).toHaveLength(1);
const analysis = analysisFor([{ file, javascript, semantic: null }]);
const context = contextFor(file, analysis);
addJavaScriptArtifactFiles(context);
addElectronIpcBoundaries(
context,
createElectronIpcContext(analysis, () => "app"),
);
const handlers = context.accumulator
.nodes()
.filter(({ kind }) => kind === "ipc-handler");
const validated = handlers.filter(({ observations }) =>
observations.some(
({ properties }) =>
properties.fact_kind === "sender-validation-candidate",
),
);
expect(validated).toHaveLength(1);
expect(
validated[0]?.observations.some(
({ properties }) => properties.channel === "inner",
),
).toBe(true);
});