diff --git a/src/domain/javascript/electronStaticAnalysisOptions.test.ts b/src/domain/javascript/electronStaticAnalysisOptions.test.ts index a6b9a922ef..e133e4dfe0 100644 --- a/src/domain/javascript/electronStaticAnalysisOptions.test.ts +++ b/src/domain/javascript/electronStaticAnalysisOptions.test.ts @@ -24,6 +24,20 @@ describe("Electron option evidence", () => { }); }); + it("does not read an audio or video preload attribute as a preload script", () => { + const analysis = analyzeJavaScriptStaticSource(` + jsx("audio", { controls: true, preload: "metadata", src }); + (0, React.createElement)(\`video\`, { preload: "auto" }); + jsx("webview", { preload: "./guest.js" }); + const options = { preload: "./preload.js" }; + `); + + expect(analysis.role_paths.map(({ path }) => path)).toEqual([ + "./guest.js", + "./preload.js", + ]); + }); + it("keeps values unknown when a later computed key can override them", () => { const analysis = analyzeJavaScriptStaticSource(` new BrowserWindow({ webPreferences: { preload: "./maybe.js" }, [key]: null }); diff --git a/src/domain/javascript/javascriptStaticAnalysisCalls.ts b/src/domain/javascript/javascriptStaticAnalysisCalls.ts index 5137e1ebd7..50dd2f17cc 100644 --- a/src/domain/javascript/javascriptStaticAnalysisCalls.ts +++ b/src/domain/javascript/javascriptStaticAnalysisCalls.ts @@ -65,6 +65,8 @@ export const inspectCall = ( addReference(context, { node, kind: "service-worker", specifier: first }); inspectEndpointCall(node, name, first, context); inspectStorageCall(node, name, first, context); + if (first === "audio" || first === "video") + noteMediaElementPreloads(node, accumulator); if (name.endsWith("loadFile") && first !== undefined) addLocatedFinding(context, { collection: accumulator.roles, @@ -95,6 +97,23 @@ export const inspectCall = ( }); }; +// Element factories such as jsx("audio", props) and createElement("video", +// props) carry the HTML media `preload` hint ("none", "metadata", "auto"), +// which never names an Electron preload script. +const noteMediaElementPreloads = ( + node: JavaScriptCallLike, + accumulator: AnalysisAccumulator, +): void => { + const props = node.arguments[1]; + if (!t.isObjectExpression(props)) return; + for (const property of props.properties) + if ( + t.isObjectProperty(property) && + semanticStaticPropertyName(property.key, property.computed) === "preload" + ) + accumulator.mediaElementPreloads.add(property); +}; + const requireCallSpecifier = ( node: JavaScriptCallLike, name: string, @@ -201,6 +220,7 @@ export const inspectRoleProperty = ( context: FindingContext, ): void => { if (semanticStaticPropertyName(node.key, node.computed) !== "preload") return; + if (context.accumulator.mediaElementPreloads.has(node)) return; const path = staticPath(node.value); if (path === undefined) return; addLocatedFinding(context, { diff --git a/src/domain/javascript/javascriptStaticAnalysisState.ts b/src/domain/javascript/javascriptStaticAnalysisState.ts index 47e25bccdb..297cbccd47 100644 --- a/src/domain/javascript/javascriptStaticAnalysisState.ts +++ b/src/domain/javascript/javascriptStaticAnalysisState.ts @@ -47,6 +47,8 @@ export interface JavaScriptAnalysisAccumulator { readonly utilityProcesses: LocatedJavaScriptFinding[]; readonly nativeAddonBindings: LocatedJavaScriptFinding[]; readonly moduleRangeIndex: JavaScriptModuleRangeIndex; + /** `preload` attributes of audio and video elements, which are not scripts. */ + readonly mediaElementPreloads: WeakSet; readonly seen: Set; visitedNodes: number; unknownFindings: number; @@ -111,6 +113,7 @@ export const createJavaScriptAnalysisAccumulator = ( utilityProcesses: [], nativeAddonBindings: [], moduleRangeIndex: new JavaScriptModuleRangeIndex(sourceLength), + mediaElementPreloads: new WeakSet(), seen: new Set(), visitedNodes: 0, unknownFindings: 0,