mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(plugins): bound marketplace manifest file reads (#101774)
* fix(plugins): bound marketplace manifest file reads * fix(plugins): distinguish oversized marketplace manifest from other read failures * fix(plugins): follow symlinked marketplace manifests while bounding reads * test(plugins): cover oversized symlinked marketplace manifest target
This commit is contained in:
@@ -305,6 +305,67 @@ describe("marketplace plugins", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects oversized local marketplace manifests", async () => {
|
||||
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
|
||||
const manifestPath = path.join(rootDir, ".claude-plugin", "marketplace.json");
|
||||
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
||||
await fs.writeFile(manifestPath, Buffer.alloc(16 * 1024 * 1024 + 1, "x"));
|
||||
|
||||
const result = await listMarketplacePlugins({ marketplace: rootDir });
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
error: "Marketplace manifest too large",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("follows a symlinked marketplace manifest to a regular file", async () => {
|
||||
if (process.platform === "win32") {
|
||||
// Symlink support in unit tests is not guaranteed on Windows CI runners.
|
||||
return;
|
||||
}
|
||||
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
|
||||
const manifestPath = path.join(rootDir, ".claude-plugin", "marketplace.json");
|
||||
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
||||
const targetPath = path.join(rootDir, "real-manifest.json");
|
||||
await fs.writeFile(
|
||||
targetPath,
|
||||
JSON.stringify({ plugins: [{ name: "symlinked-plugin", source: "." }] }),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.symlink(targetPath, manifestPath);
|
||||
|
||||
const result = await listMarketplacePlugins({ marketplace: rootDir });
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(
|
||||
(result as { ok: true; manifest: { plugins: unknown[] } }).manifest.plugins,
|
||||
).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a symlinked marketplace manifest whose target exceeds the size limit", async () => {
|
||||
if (process.platform === "win32") {
|
||||
// Symlink support in unit tests is not guaranteed on Windows CI runners.
|
||||
return;
|
||||
}
|
||||
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
|
||||
const manifestPath = path.join(rootDir, ".claude-plugin", "marketplace.json");
|
||||
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
||||
const targetPath = path.join(rootDir, "real-manifest.json");
|
||||
await fs.writeFile(targetPath, Buffer.alloc(16 * 1024 * 1024 + 1, "x"));
|
||||
await fs.symlink(targetPath, manifestPath);
|
||||
|
||||
const result = await listMarketplacePlugins({ marketplace: rootDir });
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
error: "Marketplace manifest too large",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves relative plugin paths against the marketplace root", async () => {
|
||||
await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
|
||||
const pluginDir = path.join(rootDir, "plugins", "frontend-design");
|
||||
|
||||
@@ -13,6 +13,7 @@ import { resolveOsHomeRelativePath } from "../infra/home-dir.js";
|
||||
import { tryReadJson } from "../infra/json-files.js";
|
||||
import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
|
||||
import { isPathInside } from "../infra/path-guards.js";
|
||||
import { readRegularFile } from "../infra/regular-file.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import type { InstallPolicySource } from "../security/install-policy.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
@@ -23,6 +24,7 @@ import { installPluginFromPath, type InstallPluginResult } from "./install.js";
|
||||
const DEFAULT_GIT_TIMEOUT_MS = 120_000;
|
||||
const DEFAULT_MARKETPLACE_DOWNLOAD_TIMEOUT_MS = 120_000;
|
||||
const MAX_MARKETPLACE_ARCHIVE_BYTES = 256 * 1024 * 1024;
|
||||
const MAX_MARKETPLACE_MANIFEST_BYTES = 16 * 1024 * 1024;
|
||||
const MARKETPLACE_MANIFEST_CANDIDATES = [
|
||||
path.join(".claude-plugin", "marketplace.json"),
|
||||
"marketplace.json",
|
||||
@@ -597,7 +599,28 @@ async function loadMarketplace(params: {
|
||||
remoteRef?: string;
|
||||
cleanup?: () => Promise<void>;
|
||||
}): Promise<{ ok: true; marketplace: LoadedMarketplace } | { ok: false; error: string }> => {
|
||||
const raw = await fs.readFile(paramsLocal.manifestPath, "utf-8");
|
||||
let raw: string;
|
||||
try {
|
||||
// Resolve symlinks so a marketplace.json that points to a regular file
|
||||
// keeps working, while the bounded regular-file read still rejects
|
||||
// directories, FIFOs, and oversized targets.
|
||||
const resolvedManifestPath = await fs.realpath(paramsLocal.manifestPath);
|
||||
const { buffer } = await readRegularFile({
|
||||
filePath: resolvedManifestPath,
|
||||
maxBytes: MAX_MARKETPLACE_MANIFEST_BYTES,
|
||||
});
|
||||
raw = buffer.toString("utf-8");
|
||||
} catch (err) {
|
||||
await paramsLocal.cleanup?.();
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
// readRegularFile rejects symlinks/non-files and caps file size. Only the
|
||||
// size cap should be reported as an oversize manifest; other read failures
|
||||
// need their own diagnostic so users don't chase the wrong problem.
|
||||
if (message.startsWith("File exceeds")) {
|
||||
return { ok: false, error: "Marketplace manifest too large" };
|
||||
}
|
||||
return { ok: false, error: `Marketplace manifest unreadable: ${message}` };
|
||||
}
|
||||
const parsed = parseMarketplaceManifest(raw, paramsLocal.manifestPath);
|
||||
if (!parsed.ok) {
|
||||
await paramsLocal.cleanup?.();
|
||||
|
||||
Reference in New Issue
Block a user