From 1be370543f7ea39016f86ebdec80a8ceaf7f8a28 Mon Sep 17 00:00:00 2001 From: Iwakurakaoru0822 Date: Tue, 6 Oct 2026 23:02:58 -0700 Subject: [PATCH] fix(javascript): read open() endpoints only from XHR method/URL pairs Static endpoint extraction treated any `.open(_, "literal")` call as `XMLHttpRequest.open(method, url)`. In application and web bundle analysis, `fs.open("/tmp/data.txt", "r")` became the network endpoint `r` and `window.open(url, "_blank")` became the endpoint `_blank`. Read the URL only when the first argument is a literal HTTP method: a standard method in any case, or an uppercase extension token such as WebDAV `PROPFIND`. Co-Authored-By: Claude Opus 5.5 --- src/domain/javascriptStaticAnalysisHelpers.ts | 29 ++++++++++++++++++- src/domain/webBundleAnalyzer.test.ts | 16 ++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/domain/javascriptStaticAnalysisHelpers.ts b/src/domain/javascriptStaticAnalysisHelpers.ts index a1353690a..5461e5ba4 100644 --- a/src/domain/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascriptStaticAnalysisHelpers.ts @@ -344,6 +344,32 @@ const isFileIdentity = (node: t.Node | undefined): boolean => { ); }; +const XHR_METHODS = new Set([ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE", +]); + +/** + * Require a literal HTTP method: `fs.open(path, "r")` and + * `window.open(url, "_blank")` share the callee name but take no URL second. + * XHR normalizes the standard methods' case; extension methods such as + * WebDAV `PROPFIND` are conventionally uppercase tokens. + */ +const isXhrMethodArgument = (node: t.Node | null | undefined): boolean => { + const value = stringValue(node); + return ( + value !== undefined && + (XHR_METHODS.has(value.toUpperCase()) || /^[A-Z][A-Z-]*$/u.test(value)) + ); +}; + /** Select the literal URL argument for recognized network callees. */ export const endpointArgument = ( name: string, @@ -356,7 +382,8 @@ export const endpointArgument = ( ): string | undefined => { if (name === "fetch" || name.endsWith(".fetch") || name === "WebSocket") return stringValue(args[0]); - if (name.endsWith(".open") && stringValue(args[1]) !== undefined) + // `XMLHttpRequest.open(method, url)`. + if (name.endsWith(".open") && isXhrMethodArgument(args[0])) return stringValue(args[1]); if ( ["get", "post", "put", "patch", "delete", "request"].some( diff --git a/src/domain/webBundleAnalyzer.test.ts b/src/domain/webBundleAnalyzer.test.ts index f91f06569..ed1bee5d3 100644 --- a/src/domain/webBundleAnalyzer.test.ts +++ b/src/domain/webBundleAnalyzer.test.ts @@ -174,6 +174,22 @@ describe("web bundle static-analysis parity", () => { ); }); + it("reads open endpoints only from XMLHttpRequest method and URL pairs", () => { + const result = analyzeCapturedWebBundle( + inspection(` + xhr.open("GET", "/xhr-get"); + xhr.open("post", "https://xhr.example.test/submit"); + dav.open("PROPFIND", "/dav/"); + fs.open("/tmp/data.txt", "r", done); + window.open("https://help.example.test/", "_blank"); + xhr.open(method, "/dynamic-method"); + `), + ); + expect( + result.observations.endpoints.map(({ value }) => value).sort(), + ).toEqual(["/dav/", "/xhr-get", "https://xhr.example.test/submit"]); + }); + it("recognizes loadURL endpoints like static analysis", () => { const result = analyzeCapturedWebBundle( inspection('window.win.loadURL("https://embedded.test/app");'),