From ec6fdc9b3081a433981d409c6c30acfc7eb4aede Mon Sep 17 00:00:00 2001 From: dc-bytedance Date: Wed, 1 Jul 2026 13:23:23 +0800 Subject: [PATCH] feat: fail closed when checksums.txt is missing during install (#1503) --- scripts/install.js | 11 +++++++---- scripts/install.test.js | 25 ++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/scripts/install.js b/scripts/install.js index 88a8b74c..1b9b18b0 100644 --- a/scripts/install.js +++ b/scripts/install.js @@ -265,10 +265,9 @@ function getExpectedChecksum(archiveName, checksumsDir) { const checksumsPath = path.join(dir, "checksums.txt"); if (!fs.existsSync(checksumsPath)) { - console.error( - "[WARN] checksums.txt not found, skipping checksum verification" + throw new Error( + "[SECURITY] checksums.txt not found; refusing to install an unverified binary." ); - return null; } const content = fs.readFileSync(checksumsPath, "utf8"); @@ -286,7 +285,11 @@ function getExpectedChecksum(archiveName, checksumsDir) { } function verifyChecksum(archivePath, expectedHash) { - if (expectedHash === null) return; + if (typeof expectedHash !== "string" || expectedHash.length === 0) { + throw new Error( + "[SECURITY] missing expected checksum; refusing to install an unverified binary." + ); + } // Stream the file to avoid loading the entire archive into memory. // Archives can be 10-100MB; streaming keeps RSS constant. diff --git a/scripts/install.test.js b/scripts/install.test.js index ad669613..6899d836 100644 --- a/scripts/install.test.js +++ b/scripts/install.test.js @@ -52,11 +52,17 @@ describe("getExpectedChecksum", () => { ); }); - it("returns null when checksums.txt does not exist", () => { + it("throws [SECURITY] when checksums.txt does not exist (fail-closed)", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "checksum-test-")); // No checksums.txt in dir - const result = getExpectedChecksum("anything.tar.gz", dir); - assert.equal(result, null); + assert.throws( + () => getExpectedChecksum("anything.tar.gz", dir), + (err) => { + assert.match(err.message, /^\[SECURITY\]/); + assert.match(err.message, /checksums\.txt not found/); + return true; + } + ); }); it("skips malformed lines and still finds valid entry", () => { @@ -125,6 +131,19 @@ describe("verifyChecksum", () => { } ); }); + + it("verifyChecksum throws [SECURITY] on null/empty expectedHash (fail-closed)", () => { + const filePath = makeTmpFile("content"); + for (const expectedHash of [null, ""]) { + assert.throws( + () => verifyChecksum(filePath, expectedHash), + (err) => { + assert.match(err.message, /^\[SECURITY\]/); + return true; + } + ); + } + }); }); describe("assertAllowedHost", () => {