From 319fee4691e444d5ac9870f114f93f79ef89707b Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:30:37 +0800 Subject: [PATCH] fix(electron): select nested handlers by source coordinates --- .../javascript/ElectronBoundaryGraphIpc.ts | 47 +++++++++---------- .../JavaScriptArtifactGraphStructure.test.ts | 37 +++++++++++++++ 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/src/application/javascript/ElectronBoundaryGraphIpc.ts b/src/application/javascript/ElectronBoundaryGraphIpc.ts index 9cad1de76a..cf96a2865b 100644 --- a/src/application/javascript/ElectronBoundaryGraphIpc.ts +++ b/src/application/javascript/ElectronBoundaryGraphIpc.ts @@ -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, diff --git a/src/application/javascript/JavaScriptArtifactGraphStructure.test.ts b/src/application/javascript/JavaScriptArtifactGraphStructure.test.ts index 8af1cc5f1a..7c2d1775e7 100644 --- a/src/application/javascript/JavaScriptArtifactGraphStructure.test.ts +++ b/src/application/javascript/JavaScriptArtifactGraphStructure.test.ts @@ -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); +});