mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 07:16:13 +00:00
fix(javascript): keep configuration setting reads out of endpoints (#2402)
* fix(javascript): keep configuration setting reads out of endpoints
Static endpoint extraction reads the first literal argument of any call
named get as an HTTP request path unless the receiver is a provable keyed
collection. VS Code extension bundles read settings through
`workspace.getConfiguration(section).get(key)`, so every setting key
became a network endpoint: the bundled Git extension's dist/main.js
reported 173 network endpoints, nearly all of them setting names such as
`autofetchPeriod` and `blame.statusBarItem.enabled`.
The lexical receiver classifier now records a `configuration` fact for a
`.get` call whose receiver is a `getConfiguration(...)` call, inline or
through a binding whose only visible write is one unconditional
initializer. Minifiers emit `let` for these bindings, so the proof relies
on the recorded writes rather than the declaration kind, as tRPC client
root resolution already does. Endpoint extraction skips non-URL literals
for that fact in both application and web bundle analysis. Reassigned,
conditional, and shadowed receivers, URL or path literals, and other
clients such as `api.get("users")` keep their endpoints.
The receiver fact union was duplicated in three modules; consumers now
import the classifier's type.
On the Git extension bundle, network endpoints drop from 173 to 7 with
unchanged analysis time (2.1 s).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(javascript): require the workspace host for configuration receivers
A bare or differently scoped `getConfiguration(...)` may be an
application's own HTTP client factory, so `.get("users")` on its result
lost its endpoint. Only `workspace.getConfiguration(...)` and
`<x>.workspace.getConfiguration(...)` now produce the configuration fact.
Minifiers keep property names, so the bundled VS Code Git extension still
reports 7 network endpoints instead of 173.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(javascript): recognize default VS Code namespace imports
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
4 files changed
+159
-34
No files matched your search
@@ -247,19 +247,24 @@ export function* classifyParsedJavaScriptRpcClientRootsSteps(
|
||||
return roots;
|
||||
}
|
||||
|
||||
/** Receiver identity facts for the overloaded `.open` syntax only. */
|
||||
/**
|
||||
* Receiver identity facts for the overloaded `.open` syntax, plus
|
||||
* `configuration` for `.get` reads of a `getConfiguration(...)` result.
|
||||
*/
|
||||
export type JavaScriptOpenReceiverFact =
|
||||
| "window"
|
||||
| "indexed-db"
|
||||
| "cache-storage"
|
||||
| "local"
|
||||
| "local-indexed-db"
|
||||
| "local-cache-storage";
|
||||
| "local-cache-storage"
|
||||
| "configuration";
|
||||
|
||||
/**
|
||||
* Resolve only the lexical receiver identity needed to distinguish browser
|
||||
* `window.open` from XHR and global storage `.open` calls. This deliberately
|
||||
* builds definitions without running semantic value or provenance analysis.
|
||||
* `window.open` from XHR and global storage `.open` calls, and configuration
|
||||
* `.get` reads from HTTP client calls. This deliberately builds definitions
|
||||
* without running semantic value or provenance analysis.
|
||||
*/
|
||||
export const classifyParsedJavaScriptOpenReceivers = (
|
||||
file: ParsedJavaScriptSource,
|
||||
@@ -278,14 +283,19 @@ export function* classifyParsedJavaScriptOpenReceiversSteps(
|
||||
if (
|
||||
(!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) ||
|
||||
(!t.isMemberExpression(node.callee) &&
|
||||
!t.isOptionalMemberExpression(node.callee)) ||
|
||||
semanticStaticPropertyKey(
|
||||
node.callee.property,
|
||||
node.callee.computed,
|
||||
) !== "open" ||
|
||||
!t.isIdentifier(node.callee.object)
|
||||
!t.isOptionalMemberExpression(node.callee))
|
||||
)
|
||||
return;
|
||||
const method = semanticStaticPropertyKey(
|
||||
node.callee.property,
|
||||
node.callee.computed,
|
||||
);
|
||||
if (method === "get") {
|
||||
if (isConfigurationReceiver(state, node.callee.object))
|
||||
facts.set(node.start ?? -1, "configuration");
|
||||
return;
|
||||
}
|
||||
if (method !== "open" || !t.isIdentifier(node.callee.object)) return;
|
||||
const receiver = node.callee.object.name;
|
||||
const binding = resolveSemanticBindingState(state, node, receiver);
|
||||
if (binding === undefined) {
|
||||
@@ -351,6 +361,82 @@ export function* classifyParsedJavaScriptOpenReceiversSteps(
|
||||
return facts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a call spells VS Code's `workspace.getConfiguration(section)`, whose
|
||||
* result `.get` reads setting keys rather than request paths. The `workspace`
|
||||
* member is required: a bare or differently scoped `getConfiguration` may be
|
||||
* an application's own HTTP client factory. Minifiers keep property names, so
|
||||
* bundled `x.workspace.getConfiguration` still matches.
|
||||
*/
|
||||
const isConfigurationAccessorCall = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
node: t.Node,
|
||||
): boolean => {
|
||||
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node))
|
||||
return false;
|
||||
const callee = node.callee;
|
||||
if (
|
||||
(!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee)) ||
|
||||
semanticStaticPropertyKey(callee.property, callee.computed) !==
|
||||
"getConfiguration"
|
||||
)
|
||||
return false;
|
||||
const host = callee.object;
|
||||
const matchesSpelling = t.isIdentifier(host)
|
||||
? host.name === "workspace"
|
||||
: (t.isMemberExpression(host) || t.isOptionalMemberExpression(host)) &&
|
||||
semanticStaticPropertyKey(host.property, host.computed) === "workspace";
|
||||
if (!matchesSpelling) return false;
|
||||
const root = t.isIdentifier(host)
|
||||
? host
|
||||
: t.isMemberExpression(host) || t.isOptionalMemberExpression(host)
|
||||
? host.object
|
||||
: undefined;
|
||||
if (!t.isIdentifier(root)) return false;
|
||||
const binding = resolveSemanticBindingState(state, root, root.name);
|
||||
if (binding === undefined)
|
||||
return !semanticResolutionBlocked(state, root, root.name);
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
const origins = [
|
||||
...binding.directOrigins,
|
||||
...(initializer === undefined
|
||||
? []
|
||||
: [semanticRequireOrigin(initializer.node, state)?.origin]),
|
||||
];
|
||||
return origins.some(
|
||||
(origin) =>
|
||||
origin?.specifier === "vscode" &&
|
||||
(t.isIdentifier(host)
|
||||
? origin.importedPath.at(-1) === "workspace" ||
|
||||
initializer?.projection.at(-1) === "workspace"
|
||||
: origin.importedPath.length === 0 ||
|
||||
(origin.importedPath.length === 1 &&
|
||||
(origin.importedPath[0] === "*" ||
|
||||
origin.importedPath[0] === "default"))),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether a `.get` receiver is a configuration accessor result, either inline
|
||||
* or through a binding whose only visible write is one unconditional
|
||||
* initializer. Minifiers emit `let` for such bindings, so declaration kind
|
||||
* alone does not decide.
|
||||
*/
|
||||
const isConfigurationReceiver = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
receiver: t.Node,
|
||||
): boolean => {
|
||||
if (isConfigurationAccessorCall(state, receiver)) return true;
|
||||
if (!t.isIdentifier(receiver)) return false;
|
||||
const binding = resolveSemanticBindingState(state, receiver, receiver.name);
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
return (
|
||||
initializer !== undefined &&
|
||||
initializer.projection.length === 0 &&
|
||||
isConfigurationAccessorCall(state, initializer.node)
|
||||
);
|
||||
};
|
||||
|
||||
const openGlobalFact = (
|
||||
name: string,
|
||||
): JavaScriptOpenReceiverFact | undefined => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
semanticStaticPropertyName,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import type { JavaScriptOpenReceiverFact } from "./javascriptSemanticAnalysis.js";
|
||||
import {
|
||||
compareUnicodeCodePoints,
|
||||
compositeKey,
|
||||
@@ -470,13 +471,7 @@ const xhrOpenUrl = (
|
||||
name: string,
|
||||
methodNode: t.Node | null | undefined,
|
||||
urlNode: t.Node | null | undefined,
|
||||
receiverFact?:
|
||||
| "window"
|
||||
| "indexed-db"
|
||||
| "cache-storage"
|
||||
| "local"
|
||||
| "local-indexed-db"
|
||||
| "local-cache-storage",
|
||||
receiverFact?: JavaScriptOpenReceiverFact,
|
||||
): string | undefined => {
|
||||
const url = stringValue(urlNode);
|
||||
if (url === undefined) return undefined;
|
||||
@@ -555,13 +550,7 @@ export const endpointArgument = (
|
||||
| t.ArgumentPlaceholder
|
||||
)[],
|
||||
callee: t.Node,
|
||||
receiverFact?:
|
||||
| "window"
|
||||
| "indexed-db"
|
||||
| "cache-storage"
|
||||
| "local"
|
||||
| "local-indexed-db"
|
||||
| "local-cache-storage",
|
||||
receiverFact?: JavaScriptOpenReceiverFact,
|
||||
): string | undefined => {
|
||||
if (receiverFact === "window") return undefined;
|
||||
if (name === "fetch" || name.endsWith(".fetch") || name === "WebSocket")
|
||||
@@ -575,10 +564,12 @@ export const endpointArgument = (
|
||||
const value = stringValue(args[0]);
|
||||
// Only get and delete also belong to the keyed-collection APIs, whose
|
||||
// keys are header, field, and parameter names rather than URLs or paths.
|
||||
// Configuration `get` reads setting keys.
|
||||
return value !== undefined &&
|
||||
(method === "get" || method === "delete") &&
|
||||
!isRequestUrlLiteral(value) &&
|
||||
isKeyedCollectionReceiver(callee)
|
||||
(isKeyedCollectionReceiver(callee) ||
|
||||
(method === "get" && receiverFact === "configuration"))
|
||||
? undefined
|
||||
: value;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type * as t from "@babel/types";
|
||||
import type { JavaScriptOpenReceiverFact } from "./javascriptSemanticAnalysis.js";
|
||||
import type { JavaScriptRpcScan } from "./javascriptRpcEndpoints.js";
|
||||
|
||||
import type {
|
||||
@@ -64,15 +65,7 @@ export interface JavaScriptFindingContext {
|
||||
readonly rpcScan: JavaScriptRpcScan;
|
||||
readonly source: string;
|
||||
readonly accumulator: JavaScriptAnalysisAccumulator;
|
||||
readonly openReceiverFacts?: ReadonlyMap<
|
||||
number,
|
||||
| "window"
|
||||
| "indexed-db"
|
||||
| "cache-storage"
|
||||
| "local"
|
||||
| "local-indexed-db"
|
||||
| "local-cache-storage"
|
||||
>;
|
||||
readonly openReceiverFacts?: ReadonlyMap<number, JavaScriptOpenReceiverFact>;
|
||||
/** Proven Electron export path segments by root offset. */
|
||||
readonly electronBindings?: ReadonlyMap<number, readonly string[]>;
|
||||
}
|
||||
|
||||
@@ -321,6 +321,61 @@ describe("web bundle API classification parity", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps configuration setting reads out of endpoints in both analyzers", () => {
|
||||
const source = `
|
||||
import vscodeDefault from "vscode";
|
||||
vscodeDefault.workspace.getConfiguration("git").get("default-setting");
|
||||
vscode.workspace.getConfiguration("git").get("autofetch");
|
||||
workspace.getConfiguration("git").get("enabled", true);
|
||||
const config = vscode.workspace.getConfiguration("git");
|
||||
config.get("autofetchPeriod", 180);
|
||||
config.get("/v1/settings");
|
||||
let c = Mt.workspace.getConfiguration("git", uri), l = c.get("timeline.date");
|
||||
let changed = vscode.workspace.getConfiguration("git");
|
||||
changed = api;
|
||||
changed.get("reassigned");
|
||||
const maybe = flag ? vscode.workspace.getConfiguration("git") : api;
|
||||
maybe.get("conditional");
|
||||
{
|
||||
const config = api;
|
||||
config.get("shadowed");
|
||||
}
|
||||
api.get("users");
|
||||
const client = getConfiguration("/api/v1");
|
||||
client.get("accounts");
|
||||
http.getConfiguration(baseUrl).get("orders");
|
||||
{
|
||||
const workspace = { getConfiguration: () => api };
|
||||
workspace.getConfiguration("/api").get("local-users");
|
||||
}
|
||||
{
|
||||
const vscode = { workspace: { getConfiguration: () => api } };
|
||||
vscode.workspace.getConfiguration("/api").get("local-orders");
|
||||
}
|
||||
`;
|
||||
const expected = [
|
||||
"/v1/settings",
|
||||
"accounts",
|
||||
"conditional",
|
||||
"local-orders",
|
||||
"local-users",
|
||||
"orders",
|
||||
"reassigned",
|
||||
"shadowed",
|
||||
"users",
|
||||
];
|
||||
expect(
|
||||
analyzeCapturedWebBundle(inspection(source))
|
||||
.observations.endpoints.map(({ value }) => value)
|
||||
.sort(),
|
||||
).toEqual(expected);
|
||||
expect(
|
||||
analyzeJavaScriptStaticSource(source)
|
||||
.endpoints.map(({ value }) => value)
|
||||
.sort(),
|
||||
).toEqual(expected);
|
||||
});
|
||||
|
||||
it("recognizes loadURL endpoints like static analysis", () => {
|
||||
const result = analyzeCapturedWebBundle(
|
||||
inspection('window.win.loadURL("https://embedded.test/app");'),
|
||||
|
||||
Reference in new issue
Block a user