From 982e2cf0efb2e92e9307d6ecb4d635db63a5343f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 28 May 2026 12:11:50 -0400 Subject: [PATCH] fix: reject malformed marketplace content length --- src/plugins/marketplace.test.ts | 44 +++++++++++++++++++++++++++++++++ src/plugins/marketplace.ts | 16 ++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/plugins/marketplace.test.ts b/src/plugins/marketplace.test.ts index 8d9c24aa84f..51bc261bf2b 100644 --- a/src/plugins/marketplace.test.ts +++ b/src/plugins/marketplace.test.ts @@ -824,6 +824,50 @@ describe("marketplace plugins", () => { }); }); + it("rejects malformed archive content-length headers before streaming", async () => { + await withTempDir("openclaw-marketplace-test-", async (rootDir) => { + const reader = { + read: vi.fn(), + cancel: vi.fn(async () => undefined), + releaseLock: vi.fn(), + }; + fetchWithSsrFGuardMock.mockResolvedValueOnce({ + response: { + ok: true, + status: 200, + body: { + getReader: () => reader, + } as unknown as Response["body"], + headers: new Headers({ "content-length": "1e9" }), + } as unknown as Response, + finalUrl: "https://cdn.example.com/releases/frontend-design.tgz", + release: vi.fn(async () => undefined), + }); + const manifestPath = await writeMarketplaceManifest(rootDir, { + plugins: [ + { + name: "frontend-design", + source: "https://example.com/frontend-design.tgz", + }, + ], + }); + + const result = await installPluginFromMarketplace({ + marketplace: manifestPath, + plugin: "frontend-design", + }); + + expect(result).toEqual({ + ok: false, + error: + "failed to download https://example.com/frontend-design.tgz: " + + "invalid content-length header: 1e9", + }); + expect(reader.read).not.toHaveBeenCalled(); + expect(installPluginFromPathMock).not.toHaveBeenCalled(); + }); + }); + it("cleans up a partial download temp dir when streaming the archive fails", async () => { await withTempDir("openclaw-marketplace-test-", async (rootDir) => { const beforeTempDirs = await listMarketplaceDownloadTempDirs(); diff --git a/src/plugins/marketplace.ts b/src/plugins/marketplace.ts index b9fa6a18773..92c8a108720 100644 --- a/src/plugins/marketplace.ts +++ b/src/plugins/marketplace.ts @@ -619,6 +619,18 @@ function hasStreamingResponseBody( ); } +function parseMarketplaceContentLength(raw: string): number { + const trimmed = raw.trim(); + if (!/^\d+$/.test(trimmed)) { + throw new Error(`invalid content-length header: ${raw}`); + } + const size = Number(trimmed); + if (!Number.isSafeInteger(size)) { + throw new Error(`invalid content-length header: ${raw}`); + } + return size; +} + async function readMarketplaceChunkWithTimeout( reader: ReadableStreamDefaultReader, chunkTimeoutMs: number, @@ -755,8 +767,8 @@ async function downloadUrlToTempFile( const contentLength = response.headers.get("content-length"); if (contentLength) { - const size = Number(contentLength); - if (Number.isFinite(size) && size > MAX_MARKETPLACE_ARCHIVE_BYTES) { + const size = parseMarketplaceContentLength(contentLength); + if (size > MAX_MARKETPLACE_ARCHIVE_BYTES) { throw new Error( `download too large: ${size} bytes (limit: ${MAX_MARKETPLACE_ARCHIVE_BYTES} bytes)`, );