fix: parse signal archive length strictly

This commit is contained in:
Peter Steinberger
2026-05-28 14:23:22 -04:00
parent b87510957f
commit 2afff85ca4
2 changed files with 23 additions and 1 deletions

View File

@@ -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<Uint8Array>({
start(controller) {

View File

@@ -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}).`,