From 2afff85ca43c46a075f1fce24cf1930e2e33da5c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 28 May 2026 14:23:22 -0400 Subject: [PATCH] fix: parse signal archive length strictly --- .../signal/src/install-signal-cli.test.ts | 18 ++++++++++++++++++ extensions/signal/src/install-signal-cli.ts | 6 +++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/extensions/signal/src/install-signal-cli.test.ts b/extensions/signal/src/install-signal-cli.test.ts index 744306eea26..4eca0c07a9d 100644 --- a/extensions/signal/src/install-signal-cli.test.ts +++ b/extensions/signal/src/install-signal-cli.test.ts @@ -219,6 +219,24 @@ describe("downloadToFile", () => { expect(fetchResult.release).toHaveBeenCalledTimes(1); }); + it.each(["1e3", "0x10", `1${"0".repeat(309)}`])( + "ignores malformed declared archive lengths: %s", + async (contentLength) => { + const fetchResult = okDownloadResponse("archive", { + headers: { "content-length": contentLength }, + }); + fetchWithSsrFGuardMock.mockResolvedValue(fetchResult); + + await withTempFile(async (filePath) => { + await downloadToFile("https://example.com/signal-cli.tgz", filePath, 5, 8); + + await expect(fs.readFile(filePath, "utf-8")).resolves.toBe("archive"); + }); + + expect(fetchResult.release).toHaveBeenCalledTimes(1); + }, + ); + it("aborts streamed archives above the download cap and removes partial files", async () => { const body = new ReadableStream({ start(controller) { diff --git a/extensions/signal/src/install-signal-cli.ts b/extensions/signal/src/install-signal-cli.ts index 723d254bb0c..9d4d819d62d 100644 --- a/extensions/signal/src/install-signal-cli.ts +++ b/extensions/signal/src/install-signal-cli.ts @@ -29,6 +29,7 @@ type ReleaseResponse = { const MAX_SIGNAL_CLI_ARCHIVE_BYTES = 256 * 1024 * 1024; const SIGNAL_CLI_DOWNLOAD_TIMEOUT_MS = 5 * 60_000; const SIGNAL_CLI_RELEASE_INFO_TIMEOUT_MS = 30_000; +const CONTENT_LENGTH_RE = /^\d+$/; export type SignalInstallResult = { ok: boolean; @@ -136,7 +137,10 @@ export async function downloadToFile( const rawLength = response.headers.get("content-length"); if (rawLength !== null) { - const declaredLength = Number(rawLength); + const trimmedLength = rawLength.trim(); + const declaredLength = CONTENT_LENGTH_RE.test(trimmedLength) + ? Number(trimmedLength) + : Number.NaN; if (Number.isFinite(declaredLength) && declaredLength > maxBytes) { throw new Error( `signal-cli archive exceeds the ${maxBytes}-byte download cap (declared ${declaredLength}).`,