Merge pull request #874 from morluto/fix/javascript-http-method-endpoints

fix(javascript): keep keyed-collection reads out of endpoints
This commit is contained in:
Kaoru0822 2026-10-06 23:40:24 -07:00 committed by GitHub
commit 9075ce71ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 77 additions and 8 deletions

View File

@ -162,7 +162,7 @@ const inspectEndpointCall = (
value: first,
mechanism: `call:${name}`,
});
const endpoint = endpointArgumentHelper(name, node.arguments);
const endpoint = endpointArgumentHelper(name, node.arguments, node.callee);
if (endpoint !== undefined)
addEndpoint(context, {
node,

View File

@ -370,6 +370,43 @@ const isXhrMethodArgument = (node: t.Node | null | undefined): boolean => {
);
};
const KEYED_COLLECTION_CONSTRUCTORS = new Set([
"FormData",
"Headers",
"Map",
"Set",
"URLSearchParams",
"WeakMap",
"WeakSet",
]);
/**
* Whether a get/delete receiver is provably a keyed collection rather than an
* HTTP client: a constructed Map, Set, Headers, FormData, or URLSearchParams,
* or the platform `headers`/`searchParams` properties of a request, response,
* or URL. Other receivers stay possible clients.
*/
const isKeyedCollectionReceiver = (callee: t.Node): boolean => {
if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee))
return false;
const receiver = callee.object;
if (t.isNewExpression(receiver))
return (
t.isIdentifier(receiver.callee) &&
KEYED_COLLECTION_CONSTRUCTORS.has(receiver.callee.name)
);
if (
!t.isMemberExpression(receiver) &&
!t.isOptionalMemberExpression(receiver)
)
return false;
const property = semanticStaticPropertyName(
receiver.property,
receiver.computed,
);
return property === "headers" || property === "searchParams";
};
/** Select the literal URL argument for recognized network callees. */
export const endpointArgument = (
name: string,
@ -379,18 +416,22 @@ export const endpointArgument = (
| t.JSXNamespacedName
| t.ArgumentPlaceholder
)[],
callee: t.Node,
): 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 (
["get", "post", "put", "patch", "delete", "request"].some(
(method) => name === method || name.endsWith(`.${method}`),
)
)
return stringValue(args[0]);
const method = ["get", "post", "put", "patch", "delete", "request"].find(
(candidate) => name === candidate || name.endsWith(`.${candidate}`),
);
if (method !== undefined)
// Only get and delete also belong to the keyed-collection APIs.
return (method === "get" || method === "delete") &&
isKeyedCollectionReceiver(callee)
? undefined
: stringValue(args[0]);
if (name.endsWith("loadURL")) return stringValue(args[0]);
return undefined;
};

View File

@ -190,6 +190,34 @@ describe("web bundle static-analysis parity", () => {
).toEqual(["/dav/", "/xhr-get", "https://xhr.example.test/submit"]);
});
it("keeps HTTP-method-named reads of provable keyed collections out of endpoints", () => {
const result = analyzeCapturedWebBundle(
inspection(`
axios.get("https://api.example.test/users");
api.get("users");
client.delete("cache-key");
new Map().get("map-key");
new URLSearchParams(location.search).get("q");
new Headers(init).delete("x-trace");
new FormData(form).get("file");
response.headers.get("content-type");
request?.headers.delete("cookie");
new URL(location.href).searchParams.get("page");
api.headers.post("/v1/headers");
api.searchParams.request("/search");
`),
);
expect(
result.observations.endpoints.map(({ value }) => value).sort(),
).toEqual([
"/search",
"/v1/headers",
"cache-key",
"https://api.example.test/users",
"users",
]);
});
it("recognizes loadURL endpoints like static analysis", () => {
const result = analyzeCapturedWebBundle(
inspection('window.win.loadURL("https://embedded.test/app");'),

View File

@ -147,7 +147,7 @@ const inspectCall = (
accumulator,
});
}
const endpoint = endpointArgument(name, node.arguments);
const endpoint = endpointArgument(name, node.arguments, node.callee);
if (endpoint !== undefined)
addFinding({
collection: accumulator.endpoints,