Skip to content

Commit e127679

Browse files
committed
fix(security): bound plugin manifest reads in audit deep scan
1 parent b2e42e3 commit e127679

2 files changed

Lines changed: 37 additions & 2 deletions

File tree

src/security/audit-extra.async.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,25 @@ description: test skill
246246
).toBe(false);
247247
});
248248

249+
it("surfaces manifest_parse_error finding when plugin package.json exceeds the size limit", async () => {
250+
const tmpDir = await makeTmpDir("audit-manifest-oversized");
251+
const pluginDir = path.join(tmpDir, "extensions", "oversized-plugin");
252+
await fs.mkdir(pluginDir, { recursive: true });
253+
// Oversized manifest — simulates a plugin trying to exhaust the audit reader
254+
// by declaring a huge package.json, hiding its declared extension entrypoints.
255+
await fs.writeFile(path.join(pluginDir, "package.json"), "x".repeat(1024 * 1024 + 1), "utf-8");
256+
257+
const findings = await collectPluginsCodeSafetyFindings({ stateDir: tmpDir });
258+
const finding = requireFinding(
259+
findings,
260+
(f) => f.checkId === "plugins.code_safety.manifest_parse_error",
261+
"oversized manifest parse error",
262+
);
263+
expect(finding.severity).toBe("warn");
264+
expect(finding.detail).toContain("oversized-plugin");
265+
expect(finding.detail).toContain("too large");
266+
});
267+
249268
it("reports scan_failed when plugin code scanner throws during deep audit", async () => {
250269
const scanSpy = vi
251270
.spyOn(skillScanner, "scanDirectoryWithSummary")

src/security/audit-extra.async.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
*
44
* These functions perform I/O (filesystem, config reads) to detect security issues.
55
*/
6-
import fs from "node:fs/promises";
76
import path from "node:path";
87
import { clearTimeout as clearNodeTimeout, setTimeout as setNodeTimeout } from "node:timers";
98
import {
@@ -21,6 +20,7 @@ import { MANIFEST_KEY } from "../compat/legacy-names.js";
2120
import type { OpenClawConfig, ConfigFileSnapshot } from "../config/config.js";
2221
import { collectIncludePathsRecursive } from "../config/includes-scan.js";
2322
import { resolveOAuthDir } from "../config/paths.js";
23+
import { readRegularFile, statRegularFile } from "../infra/fs-safe.js";
2424
import { normalizeAgentId } from "../routing/session-key.js";
2525
import { createLazyRuntimeModule, createLazyRuntimeNamedExport } from "../shared/lazy-runtime.js";
2626
import type { SkillScanFinding } from "../skills/security/scanner.js";
@@ -96,9 +96,25 @@ function expandTilde(p: string, env: NodeJS.ProcessEnv): string | null {
9696
return null;
9797
}
9898

99+
const MAX_PLUGIN_MANIFEST_BYTES = 1024 * 1024;
100+
99101
async function readPluginManifestExtensions(pluginPath: string): Promise<string[]> {
100102
const manifestPath = path.join(pluginPath, "package.json");
101-
const raw = await fs.readFile(manifestPath, "utf-8").catch(() => "");
103+
const statResult = await statRegularFile(manifestPath);
104+
if (statResult.missing) {
105+
return [];
106+
}
107+
if (statResult.stat.size > MAX_PLUGIN_MANIFEST_BYTES) {
108+
throw new Error(
109+
`Plugin manifest at ${manifestPath} is too large (${statResult.stat.size} bytes, max ${MAX_PLUGIN_MANIFEST_BYTES})`,
110+
);
111+
}
112+
113+
const { buffer } = await readRegularFile({
114+
filePath: manifestPath,
115+
maxBytes: MAX_PLUGIN_MANIFEST_BYTES,
116+
});
117+
const raw = buffer.toString("utf-8");
102118
if (!raw.trim()) {
103119
return [];
104120
}

0 commit comments

Comments
 (0)