mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 08:17:02 +00:00
fix(javascript): keep keyed-collection reads out of endpoints
Static endpoint extraction read the first literal argument of any call
named get, post, put, patch, delete, or request as an HTTP request URL.
Reads of a provable keyed collection also became network endpoints in
application and web bundle analysis: `new URLSearchParams(...).get("q")`,
`response.headers.get("content-type")`, `new Map().get("key")`, and
`url.searchParams.get("page")`.
Skip these calls only when the receiver is a constructed Map, WeakMap,
Set, WeakSet, Headers, FormData, or URLSearchParams, or a platform
`headers`/`searchParams` property. Any other receiver may still be an
HTTP client with a base URL, so `api.get("users")` keeps its endpoint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8e8921d40c
commit
eb68761eb0
4 files changed
+61
-3
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,6 +416,7 @@ export const endpointArgument = (
|
||||
| t.JSXNamespacedName
|
||||
| t.ArgumentPlaceholder
|
||||
)[],
|
||||
callee: t.Node,
|
||||
): string | undefined => {
|
||||
if (name === "fetch" || name.endsWith(".fetch") || name === "WebSocket")
|
||||
return stringValue(args[0]);
|
||||
@@ -390,7 +428,7 @@ export const endpointArgument = (
|
||||
(method) => name === method || name.endsWith(`.${method}`),
|
||||
)
|
||||
)
|
||||
return stringValue(args[0]);
|
||||
return isKeyedCollectionReceiver(callee) ? undefined : stringValue(args[0]);
|
||||
if (name.endsWith("loadURL")) return stringValue(args[0]);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -190,6 +190,26 @@ 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");
|
||||
`),
|
||||
);
|
||||
expect(
|
||||
result.observations.endpoints.map(({ value }) => value).sort(),
|
||||
).toEqual(["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");'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user