fix(security): bound plugin manifest reads in audit deep scan (#101773)

This commit is contained in:
cxbAsDev
2026-07-18 16:55:31 +08:00
committed by GitHub
parent 654560f289
commit aa0c92186b
2 changed files with 37 additions and 2 deletions

View File

@@ -295,6 +295,25 @@ Read the requested file and summarize it.
).toBe(false);
});
it("surfaces manifest_parse_error finding when plugin package.json exceeds the size limit", async () => {
const tmpDir = await makeTmpDir("audit-manifest-oversized");
const pluginDir = path.join(tmpDir, "extensions", "oversized-plugin");
await fs.mkdir(pluginDir, { recursive: true });
// Oversized manifest — simulates a plugin trying to exhaust the audit reader
// by declaring a huge package.json, hiding its declared extension entrypoints.
await fs.writeFile(path.join(pluginDir, "package.json"), "x".repeat(1024 * 1024 + 1), "utf-8");
const findings = await collectPluginsCodeSafetyFindings({ stateDir: tmpDir });
const finding = requireFinding(
findings,
(f) => f.checkId === "plugins.code_safety.manifest_parse_error",
"oversized manifest parse error",
);
expect(finding.severity).toBe("warn");
expect(finding.detail).toContain("oversized-plugin");
expect(finding.detail).toContain("too large");
});
it("reports scan_failed when plugin code scanner throws during deep audit", async () => {
const scanSpy = vi
.spyOn(skillScanner, "scanDirectoryWithSummary")

View File

@@ -3,7 +3,6 @@
*
* These functions perform I/O (filesystem, config reads) to detect security issues.
*/
import fs from "node:fs/promises";
import path from "node:path";
import { clearTimeout as clearNodeTimeout, setTimeout as setNodeTimeout } from "node:timers";
import {
@@ -21,6 +20,7 @@ import { MANIFEST_KEY } from "../compat/legacy-names.js";
import type { OpenClawConfig, ConfigFileSnapshot } from "../config/config.js";
import { collectIncludePathsRecursive } from "../config/includes-scan.js";
import { resolveOAuthDir } from "../config/paths.js";
import { readRegularFile, statRegularFile } from "../infra/fs-safe.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { getOrCreatePromise } from "../shared/lazy-promise.js";
import { createLazyRuntimeModule, createLazyRuntimeNamedExport } from "../shared/lazy-runtime.js";
@@ -97,9 +97,25 @@ function expandTilde(p: string, env: NodeJS.ProcessEnv): string | null {
return null;
}
const MAX_PLUGIN_MANIFEST_BYTES = 1024 * 1024;
async function readPluginManifestExtensions(pluginPath: string): Promise<string[]> {
const manifestPath = path.join(pluginPath, "package.json");
const raw = await fs.readFile(manifestPath, "utf-8").catch(() => "");
const statResult = await statRegularFile(manifestPath);
if (statResult.missing) {
return [];
}
if (statResult.stat.size > MAX_PLUGIN_MANIFEST_BYTES) {
throw new Error(
`Plugin manifest at ${manifestPath} is too large (${statResult.stat.size} bytes, max ${MAX_PLUGIN_MANIFEST_BYTES})`,
);
}
const { buffer } = await readRegularFile({
filePath: manifestPath,
maxBytes: MAX_PLUGIN_MANIFEST_BYTES,
});
const raw = buffer.toString("utf-8");
if (!raw.trim()) {
return [];
}