From 602d0021d45a6cdab9f806554f3ee12ea670ec97 Mon Sep 17 00:00:00 2001 From: Iwakurakaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:25:48 -0700 Subject: [PATCH 1/3] fix(javascript): keep computed-method XHR request URLs A computed XHR method, as in `xhr.open(method, "/api")`, no longer discarded an endpoint that main reports. Keep the URL when it is an HTTP(S)/WebSocket URL or a path; fs flags and window targets such as `r` and `_blank` still produce no endpoint. Co-Authored-By: Claude Opus 5.5 --- src/domain/javascriptStaticAnalysisHelpers.ts | 33 ++++++++++++------- src/domain/webBundleAnalyzer.test.ts | 9 ++++- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/domain/javascriptStaticAnalysisHelpers.ts b/src/domain/javascriptStaticAnalysisHelpers.ts index 5461e5ba4..8400862a1 100644 --- a/src/domain/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascriptStaticAnalysisHelpers.ts @@ -356,18 +356,31 @@ const XHR_METHODS = new Set([ "TRACE", ]); +/** An absolute HTTP(S)/WebSocket URL, or an absolute or relative path. */ +const isRequestUrlLiteral = (value: string): boolean => + /^(?:https?|wss?):\/\//iu.test(value) || + value.startsWith("/") || + value.startsWith("./") || + value.startsWith("../"); + /** - * Require a literal HTTP method: `fs.open(path, "r")` and - * `window.open(url, "_blank")` share the callee name but take no URL second. + * Read `XMLHttpRequest.open(method, url)`. `fs.open(path, "r")` and + * `window.open(url, "_blank")` share the callee name, so a literal method + * must be an HTTP method, and a computed method needs a request URL literal. * 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)) - ); +const xhrOpenUrl = ( + methodNode: t.Node | null | undefined, + urlNode: t.Node | null | undefined, +): string | undefined => { + const url = stringValue(urlNode); + if (url === undefined) return undefined; + const method = stringValue(methodNode); + if (method === undefined) return isRequestUrlLiteral(url) ? url : undefined; + return XHR_METHODS.has(method.toUpperCase()) || /^[A-Z][A-Z-]*$/u.test(method) + ? url + : undefined; }; /** Select the literal URL argument for recognized network callees. */ @@ -382,9 +395,7 @@ export const endpointArgument = ( ): string | undefined => { if (name === "fetch" || name.endsWith(".fetch") || name === "WebSocket") return stringValue(args[0]); - // `XMLHttpRequest.open(method, url)`. - if (name.endsWith(".open") && isXhrMethodArgument(args[0])) - return stringValue(args[1]); + if (name.endsWith(".open")) return xhrOpenUrl(args[0], args[1]); if ( ["get", "post", "put", "patch", "delete", "request"].some( (method) => name === method || name.endsWith(`.${method}`), diff --git a/src/domain/webBundleAnalyzer.test.ts b/src/domain/webBundleAnalyzer.test.ts index ed1bee5d3..77c5df458 100644 --- a/src/domain/webBundleAnalyzer.test.ts +++ b/src/domain/webBundleAnalyzer.test.ts @@ -183,11 +183,18 @@ describe("web bundle static-analysis parity", () => { fs.open("/tmp/data.txt", "r", done); window.open("https://help.example.test/", "_blank"); xhr.open(method, "/dynamic-method"); + fs.open(path, "r", done); + window.open(url, "_blank"); `), ); expect( result.observations.endpoints.map(({ value }) => value).sort(), - ).toEqual(["/dav/", "/xhr-get", "https://xhr.example.test/submit"]); + ).toEqual([ + "/dav/", + "/dynamic-method", + "/xhr-get", + "https://xhr.example.test/submit", + ]); }); it("recognizes loadURL endpoints like static analysis", () => { From ae849a72811857ba9755962ec48a1d76c9d1181a Mon Sep 17 00:00:00 2001 From: Iwakurakaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:32:50 -0700 Subject: [PATCH 2/3] fix(javascript): never read window or document open as XHR `window.open(url, "/preview")` has a computed first argument and a path-shaped browsing-context name, so the computed-method XHR fallback reported the target name as an endpoint. Browsing-context and document `open` calls never take an XHR method and request URL; skip them. Co-Authored-By: Claude Opus 5.5 --- src/domain/javascriptStaticAnalysisHelpers.ts | 10 +++++++++- src/domain/webBundleAnalyzer.test.ts | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/domain/javascriptStaticAnalysisHelpers.ts b/src/domain/javascriptStaticAnalysisHelpers.ts index 8400862a1..d13d2a7ac 100644 --- a/src/domain/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascriptStaticAnalysisHelpers.ts @@ -383,6 +383,13 @@ const xhrOpenUrl = ( : undefined; }; +/** + * Browsing-context and document `open` take a URL or type first and a target + * name second, never an XHR method and request URL. + */ +const WINDOW_OPEN_CALL = + /(?:^|\.)(?:window|self|globalThis|top|parent|opener|frames|document)\.open$/u; + /** Select the literal URL argument for recognized network callees. */ export const endpointArgument = ( name: string, @@ -395,7 +402,8 @@ export const endpointArgument = ( ): string | undefined => { if (name === "fetch" || name.endsWith(".fetch") || name === "WebSocket") return stringValue(args[0]); - if (name.endsWith(".open")) return xhrOpenUrl(args[0], args[1]); + if (name.endsWith(".open") && !WINDOW_OPEN_CALL.test(name)) + return xhrOpenUrl(args[0], args[1]); if ( ["get", "post", "put", "patch", "delete", "request"].some( (method) => name === method || name.endsWith(`.${method}`), diff --git a/src/domain/webBundleAnalyzer.test.ts b/src/domain/webBundleAnalyzer.test.ts index 77c5df458..d4f5440be 100644 --- a/src/domain/webBundleAnalyzer.test.ts +++ b/src/domain/webBundleAnalyzer.test.ts @@ -185,6 +185,9 @@ describe("web bundle static-analysis parity", () => { xhr.open(method, "/dynamic-method"); fs.open(path, "r", done); window.open(url, "_blank"); + window.open(url, "/preview"); + globalThis.open(url, "./frame"); + document.open("text/html", "/replace"); `), ); expect( From e32028d1b566872ba8cd08fc6800c198d6aef30b Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:01:16 -0700 Subject: [PATCH 3/3] fix(javascript): keep bare relative URLs for computed-method XHR `xhr.open(method, "api/users")` is a same-origin request, but the computed-method fallback required an absolute URL or a path prefix. Keep the literal unless it is provably not a URL: a Node fs open flag, a reserved browsing-context name, or a call on a receiver spelled as a browsing context or document. Co-Authored-By: Claude Opus 5.5 --- src/domain/javascriptStaticAnalysisHelpers.ts | 51 ++++++++++++++++--- src/domain/webBundleAnalyzer.test.ts | 4 ++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/domain/javascriptStaticAnalysisHelpers.ts b/src/domain/javascriptStaticAnalysisHelpers.ts index c7085e2af..89370e6fd 100644 --- a/src/domain/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascriptStaticAnalysisHelpers.ts @@ -371,12 +371,47 @@ const isRequestUrlLiteral = (value: string): boolean => const WINDOW_OPEN_CALL = /(?:^|\.)(?:window|self|globalThis|top|parent|opener|frames|document)\.open$/u; +/** Node `fs.open` flag strings, which are never request URLs. */ +const FS_OPEN_FLAGS = new Set([ + "a", + "a+", + "as", + "as+", + "ax", + "ax+", + "r", + "r+", + "rs", + "rs+", + "sa", + "sa+", + "sr", + "sr+", + "w", + "w+", + "wx", + "wx+", + "xa", + "xa+", + "xw", + "xw+", +]); + +/** Reserved browsing-context names (ASCII case-insensitive). */ +const BROWSING_CONTEXT_KEYWORDS = new Set([ + "_blank", + "_parent", + "_self", + "_top", +]); + /** * Read `XMLHttpRequest.open(method, url)`. `fs.open(path, "r")` and - * `window.open(url, "_blank")` share the callee name, so a literal method - * must be an HTTP method, and a computed method needs a request URL literal - * on a receiver not spelled as a browsing context. XHR normalizes the - * standard methods' case; extension methods such as WebDAV `PROPFIND` are + * `window.open(url, "_blank")` share the callee name. A literal method must + * be an HTTP method; with a computed method, the second literal is a URL + * unless it is provably an fs flag or a browsing-context keyword, or the + * receiver is spelled as a browsing context. XHR normalizes the standard + * methods' case; extension methods such as WebDAV `PROPFIND` are * conventionally uppercase tokens. */ const xhrOpenUrl = ( @@ -388,9 +423,11 @@ const xhrOpenUrl = ( if (url === undefined) return undefined; const method = stringValue(methodNode); if (method === undefined) - return !WINDOW_OPEN_CALL.test(name) && isRequestUrlLiteral(url) - ? url - : undefined; + return WINDOW_OPEN_CALL.test(name) || + FS_OPEN_FLAGS.has(url) || + BROWSING_CONTEXT_KEYWORDS.has(url.toLowerCase()) + ? undefined + : url; return XHR_METHODS.has(method.toUpperCase()) || /^[A-Z][A-Z-]*$/u.test(method) ? url : undefined; diff --git a/src/domain/webBundleAnalyzer.test.ts b/src/domain/webBundleAnalyzer.test.ts index e8f038d69..86e6bec90 100644 --- a/src/domain/webBundleAnalyzer.test.ts +++ b/src/domain/webBundleAnalyzer.test.ts @@ -190,6 +190,9 @@ describe("web bundle static-analysis parity", () => { document.open("text/html", "/replace"); parent.open("GET", "/parent-xhr"); self.open("post", "/self-xhr"); + xhr.open(method, "api/relative"); + fs.open(path, "w+", done); + popup.open(url, "_TOP"); `), ); expect( @@ -200,6 +203,7 @@ describe("web bundle static-analysis parity", () => { "/parent-xhr", "/self-xhr", "/xhr-get", + "api/relative", "https://xhr.example.test/submit", ]); });