mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-31 11:08:32 +00:00
fix: reject malformed marketplace content length
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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<Uint8Array>,
|
||||
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)`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user