




















@@ -8,6 +8,7 @@ import {
88closeSync,
99existsSync,
1010lstatSync,
11+opendirSync,
1112openSync,
1213readdirSync,
1314readFileSync,
@@ -29,6 +30,7 @@ const DEFAULT_PACKAGE_ROOT = join(scriptDir, "..");
2930const DISABLE_POSTINSTALL_ENV = "OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL";
3031const DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV = "OPENCLAW_DISABLE_PLUGIN_REGISTRY_MIGRATION";
3132const DIST_INVENTORY_PATH = "dist/postinstall-inventory.json";
33+export const MAX_INSTALLED_DIST_SCAN_ENTRIES = 25_000;
3234const LEGACY_PLUGIN_RUNTIME_DEPS_DIR = "plugin-runtime-deps";
3335const BAILEYS_MEDIA_FILE = join("node_modules", "baileys", "lib", "Utils", "messages-media.js");
3436const BAILEYS_MEDIA_HOTFIX_NEEDLE = [
@@ -107,6 +109,8 @@ const BAILEYS_MEDIA_ASYNC_CONTEXT_RE =
107109/async\s+function\s+encryptedStream|encryptedStream\s*=\s*async/u;
108110const NODE_COMPILE_CACHE_VERSION_DIR_RE = /^v\d+\.\d+\.\d+-/u;
109111112+class InstalledDistScanLimitError extends Error {}
113+110114function hasEnvFlag(env, key) {
111115const value = env?.[key]?.trim().toLowerCase();
112116return Boolean(value && value !== "0" && value !== "false" && value !== "no");
@@ -197,21 +201,72 @@ function assertSafeInstalledDistPath(relativePath, params) {
197201return candidatePath;
198202}
199203204+function createInstalledDistScanBudget(params = {}) {
205+return {
206+entries: 0,
207+limit: params.maxDistScanEntries ?? MAX_INSTALLED_DIST_SCAN_ENTRIES,
208+};
209+}
210+211+function resolveInstalledDistScanBudget(params = {}) {
212+return params.distScanBudget ?? createInstalledDistScanBudget(params);
213+}
214+215+function countInstalledDistScanEntry(budget) {
216+budget.entries += 1;
217+if (budget.entries > budget.limit) {
218+throw new InstalledDistScanLimitError(
219+`installed dist scan exceeded ${budget.limit} filesystem entries; refusing to scan unbounded package contents`,
220+);
221+}
222+}
223+224+function* iterateInstalledDistEntries(currentDir, params = {}) {
225+if (params.readdirSync) {
226+yield* params.readdirSync(currentDir, { withFileTypes: true });
227+return;
228+}
229+230+const dir = opendirSync(currentDir);
231+try {
232+while (true) {
233+const entry = dir.readSync();
234+if (!entry) {
235+break;
236+}
237+yield entry;
238+}
239+} finally {
240+dir.closeSync();
241+}
242+}
243+244+function* iterateOptionalInstalledDistEntries(currentDir, params = {}) {
245+try {
246+yield* iterateInstalledDistEntries(currentDir, params);
247+} catch (error) {
248+if (error instanceof InstalledDistScanLimitError) {
249+throw error;
250+}
251+}
252+}
253+200254function listInstalledDistFiles(params = {}) {
201-const readDir = params.readdirSync ?? readdirSync;
202255const distRoot = resolveInstalledDistRoot(params);
203256if (distRoot === null) {
204257return [];
205258}
206259const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
207260const pending = [distRoot.distDir];
208261const files = [];
262+const budget = resolveInstalledDistScanBudget(params);
209263while (pending.length > 0) {
210264const currentDir = pending.pop();
211265if (!currentDir) {
212266continue;
213267}
214-for (const entry of readDir(currentDir, { withFileTypes: true })) {
268+for (const entry of iterateInstalledDistEntries(currentDir, params)) {
269+countInstalledDistScanEntry(budget);
215270const entryPath = join(currentDir, entry.name);
216271if (entry.isSymbolicLink()) {
217272throw new Error(
@@ -236,17 +291,28 @@ function listInstalledDistFiles(params = {}) {
236291}
237292238293function pruneEmptyDistDirectories(params = {}) {
239-const readDir = params.readdirSync ?? readdirSync;
240294const removeDirectory = params.rmdirSync ?? rmdirSync;
241295const distRoot = resolveInstalledDistRoot(params);
242296if (distRoot === null) {
243297return;
244298}
245299const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
246300const pathLstat = params.lstatSync ?? lstatSync;
301+const budget = resolveInstalledDistScanBudget(params);
302+303+function isDirectoryEmpty(currentDir) {
304+for (const entry of iterateInstalledDistEntries(currentDir, params)) {
305+void entry;
306+countInstalledDistScanEntry(budget);
307+return false;
308+}
309+return true;
310+}
247311248312function prune(currentDir) {
249-for (const entry of readDir(currentDir, { withFileTypes: true })) {
313+const childDirs = [];
314+for (const entry of iterateInstalledDistEntries(currentDir, params)) {
315+countInstalledDistScanEntry(budget);
250316if (entry.isSymbolicLink()) {
251317throw new Error(
252318`unsafe dist entry: ${normalizeRelativePath(relative(packageRoot, join(currentDir, entry.name)))}`,
@@ -255,7 +321,10 @@ function pruneEmptyDistDirectories(params = {}) {
255321if (!entry.isDirectory()) {
256322continue;
257323}
258-prune(join(currentDir, entry.name));
324+childDirs.push(join(currentDir, entry.name));
325+}
326+for (const childDir of childDirs) {
327+prune(childDir);
259328}
260329if (currentDir === distRoot.distDir) {
261330return;
@@ -266,7 +335,7 @@ function pruneEmptyDistDirectories(params = {}) {
266335`unsafe dist directory: ${normalizeRelativePath(relative(packageRoot, currentDir))}`,
267336);
268337}
269-if (readDir(currentDir).length === 0) {
338+if (isDirectoryEmpty(currentDir)) {
270339removeDirectory(
271340assertSafeInstalledDistPath(normalizeRelativePath(relative(packageRoot, currentDir)), {
272341 packageRoot,
@@ -285,45 +354,42 @@ function isLegacyInstalledPluginDependencyDirName(name) {
285354}
286355287356function pruneLegacyInstalledPluginDependencyDirs(params) {
288-const readDir = params.readdirSync ?? readdirSync;
289357const removePath = params.rmSync ?? rmSync;
290358const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
291359const extensionsDir = join(packageRoot, "dist", "extensions");
360+const budget = resolveInstalledDistScanBudget(params);
292361const removed = [];
293-let pluginEntries;
294-try {
295-pluginEntries = readDir(extensionsDir, { withFileTypes: true });
296-} catch {
297-return removed;
298-}
299362300-for (const pluginEntry of pluginEntries) {
363+for (const pluginEntry of iterateOptionalInstalledDistEntries(extensionsDir, params)) {
364+countInstalledDistScanEntry(budget);
301365if (!pluginEntry.isDirectory() || pluginEntry.isSymbolicLink()) {
302366continue;
303367}
304368const pluginDir = join(extensionsDir, pluginEntry.name);
305-let pluginChildren;
306-try {
307-pluginChildren = readDir(pluginDir, { withFileTypes: true });
308-} catch {
309-continue;
310-}
311-for (const childEntry of pluginChildren) {
369+const dependencyDirNames = [];
370+for (const childEntry of iterateOptionalInstalledDistEntries(pluginDir, params)) {
371+countInstalledDistScanEntry(budget);
312372if (!isLegacyInstalledPluginDependencyDirName(childEntry.name)) {
313373continue;
314374}
315-const safePluginDir = assertSafeInstalledDistPath(
316-normalizeRelativePath(relative(packageRoot, pluginDir)),
317-{
318- packageRoot,
319-distDirReal: params.distDirReal,
320-realpathSync: params.realpathSync,
321-},
322-);
375+dependencyDirNames.push(childEntry.name);
376+}
377+if (dependencyDirNames.length === 0) {
378+continue;
379+}
380+const safePluginDir = assertSafeInstalledDistPath(
381+normalizeRelativePath(relative(packageRoot, pluginDir)),
382+{
383+ packageRoot,
384+distDirReal: params.distDirReal,
385+realpathSync: params.realpathSync,
386+},
387+);
388+for (const dependencyDirName of dependencyDirNames) {
323389const relativePath = normalizeRelativePath(
324-relative(packageRoot, join(pluginDir, childEntry.name)),
390+relative(packageRoot, join(pluginDir, dependencyDirName)),
325391);
326-removePath(join(safePluginDir, childEntry.name), { recursive: true, force: true });
392+removePath(join(safePluginDir, dependencyDirName), { recursive: true, force: true });
327393removed.push(relativePath);
328394}
329395}
@@ -502,11 +568,13 @@ export function pruneInstalledPackageDist(params = {}) {
502568if (distRoot === null) {
503569return [];
504570}
571+const distScanBudget = createInstalledDistScanBudget(params);
572+const distScanParams = { ...params, distScanBudget };
505573const removedLegacyDependencyDirs = pruneLegacyInstalledPluginDependencyDirs({
574+ ...distScanParams,
506575 packageRoot,
507576distDirReal: distRoot.distDirReal,
508577realpathSync: params.realpathSync,
509-readdirSync: params.readdirSync,
510578rmSync: params.rmSync,
511579});
512580let expectedFiles = params.expectedFiles ?? null;
@@ -521,7 +589,7 @@ export function pruneInstalledPackageDist(params = {}) {
521589return [];
522590}
523591}
524-const installedFiles = listInstalledDistFiles(params);
592+const installedFiles = listInstalledDistFiles(distScanParams);
525593const readFile = params.readFileSync ?? readFileSync;
526594expectedFiles = new Set(
527595expandPackageDistImportClosure({
@@ -555,7 +623,7 @@ export function pruneInstalledPackageDist(params = {}) {
555623removed.push(relativePath);
556624}
557625558-pruneEmptyDistDirectories(params);
626+pruneEmptyDistDirectories(distScanParams);
559627560628if (removed.length > 0) {
561629log.log(`[postinstall] pruned stale dist files: ${removed.join(", ")}`);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。