From 9cba1e77cbd16804ee485f0290cbb78035012a86 Mon Sep 17 00:00:00 2001 From: NIO Date: Mon, 29 Jun 2026 02:46:25 +0800 Subject: [PATCH] fix(signal): bound GitHub release info JSON response with readProviderJsonResponse (#97536) Replace bare `await response.json()` in `installSignalCliFromRelease` with `readProviderJsonResponse` (16 MiB cap, stream cancel on overflow). The external GitHub Releases endpoint can include a large `body` changelog field; the error path was already guarded but the success path was unbounded. The existing inner catch continues to convert overflow errors into the graceful `{ ok: false, error: "Failed to parse signal-cli release info." }` path. Adds a regression test verifying the stream is cancelled before all chunks are read on an oversized 20 MiB streaming response. Co-authored-by: NIO Co-authored-by: Cursor (cherry picked from commit 51064bda4def47da9864c63eb0bed4e2aa614afb) --- .../signal/src/install-signal-cli.test.ts | 32 +++++++++++++++++++ extensions/signal/src/install-signal-cli.ts | 3 +- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/extensions/signal/src/install-signal-cli.test.ts b/extensions/signal/src/install-signal-cli.test.ts index 74c027030aa7..d356a0bf013f 100644 --- a/extensions/signal/src/install-signal-cli.test.ts +++ b/extensions/signal/src/install-signal-cli.test.ts @@ -277,6 +277,38 @@ describe("installSignalCliFromRelease", () => { expect(fetchResult.release).toHaveBeenCalledTimes(1); }); + it("bounds oversized GitHub release metadata and cancels the stream", async () => { + const chunkSize = 1024 * 1024; + const chunkCount = 20; // 20 MiB — over the 16 MiB cap + let readCount = 0; + let canceled = false; + const oversized = new Response( + new ReadableStream({ + pull(controller) { + if (readCount >= chunkCount) { + controller.close(); + return; + } + readCount += 1; + controller.enqueue(new Uint8Array(chunkSize)); + }, + cancel() { + canceled = true; + }, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + const releaseMock = vi.fn().mockResolvedValue(undefined); + fetchWithSsrFGuardMock.mockResolvedValue({ response: oversized, release: releaseMock }); + + const result = await installSignalCliFromRelease({ log: vi.fn() } as unknown as RuntimeEnv); + + expect(result).toEqual({ ok: false, error: "Failed to parse signal-cli release info." }); + expect(canceled).toBe(true); + expect(readCount).toBeLessThan(chunkCount); + expect(releaseMock).toHaveBeenCalledTimes(1); + }); + it("bounds the release metadata request with an explicit timeout", async () => { const fetchResult = okDownloadResponse(JSON.stringify({ tag_name: "v0.14.3", assets: [] }), { headers: { "content-type": "application/json" }, diff --git a/extensions/signal/src/install-signal-cli.ts b/extensions/signal/src/install-signal-cli.ts index 3b79e653bbd0..ae0f7b471d65 100644 --- a/extensions/signal/src/install-signal-cli.ts +++ b/extensions/signal/src/install-signal-cli.ts @@ -5,6 +5,7 @@ import path from "node:path"; import { Readable, Transform } from "node:stream"; import { pipeline } from "node:stream/promises"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http"; import { runPluginCommandWithTimeout } from "openclaw/plugin-sdk/run-command"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { CONFIG_DIR, extractArchive, resolveBrewExecutable } from "openclaw/plugin-sdk/setup-tools"; @@ -303,7 +304,7 @@ export async function installSignalCliFromRelease( }; } try { - payload = (await response.json()) as ReleaseResponse; + payload = await readProviderJsonResponse(response, "signal.release-info"); } catch { return { ok: false,