From ed67c4719aec59c43b5720b7a689e992bd15bd8d Mon Sep 17 00:00:00 2001 From: Local Dev Date: Tue, 8 Sep 2026 19:52:15 +0200 Subject: [PATCH] fix(theseus/updater): verify manifest SHA-256 before arming install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The in-app updater fetched the setup .exe via session.defaultSession.downloadURL and marked updateDownloadState="ready" on any DownloadItem `done` with state === "completed", then handed that path to install-update-now to spawn. No hash check against the manifest — the same manifest that already carries a SHA-256 per file and that the Ariadne addon updater verifies at ariadneDownloadInstaller in this same file. Consequence: a mid-stream truncation the runtime swallowed as "completed" (a wrong Content-Length, a CDN cache truncation, an interrupted TLS session, a corrupted mirror) armed install of a half-file. install-update-now then ran the corrupt setup silently, NSIS integrity check failed, uninstaller wiped the app first, and Theseus was gone with nothing to click. Now the completion handler streams the saved file through crypto.createHash("sha256"), compares against updateAvailable.setupHash from the manifest (already captured in checkForUpdate), and refuses to arm install on mismatch — deletes the corrupt file and marks the download failed so the retry loop can pick a fresh one up. Companion fix to 0.3.32's --updated /S --force-run flags. Both symptoms landed users in the same "browser vanished" state; 0.3.32 covered the spawn-side, this covers the download-side. --- main.js | 48 ++++++++++++++++++++++++++++++++++++++++++------ package.json | 2 +- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/main.js b/main.js index f0077dcc..48b8ae95 100644 --- a/main.js +++ b/main.js @@ -1984,15 +1984,51 @@ function installDownloadTracker() { emitUpdateAvailable(); }); item.once("done", (_ev, state) => { - if (state === "completed") { - updateDownloadPath = item.getSavePath() || dst; - updateDownloadState = "ready"; - console.log(`[update] silent fetch complete: ${updateDownloadPath}`); - } else { + if (state !== "completed") { updateDownloadState = "failed"; console.warn(`[update] silent fetch ${state}`); + emitUpdateAvailable(); + return; } - emitUpdateAvailable(); + // NEVER mark "ready" without verifying the file hashes to what the + // manifest promised. Electron's DownloadItem has been observed to + // fire done/completed on truncated payloads (bad Content-Length, + // CDN cache truncation, mid-stream TLS reset the runtime swallowed), + // and 0.3.31's in-app updater then spawned a half-file as setup — + // NSIS integrity check failed silently and the browser was gone. + const savedPath = item.getSavePath() || dst; + const expected = String(updateAvailable && updateAvailable.setupHash || "").toLowerCase(); + if (!expected) { + updateDownloadState = "failed"; + console.warn(`[update] no manifest hash for ${savedPath} — refusing to arm install`); + try { fs.unlinkSync(savedPath); } catch {} + emitUpdateAvailable(); + return; + } + const crypto = require("node:crypto"); + const hash = crypto.createHash("sha256"); + const rs = fs.createReadStream(savedPath); + rs.on("data", (c) => hash.update(c)); + rs.once("error", (e) => { + updateDownloadState = "failed"; + console.warn(`[update] hash read failed: ${e.message}`); + try { fs.unlinkSync(savedPath); } catch {} + emitUpdateAvailable(); + }); + rs.once("end", () => { + const got = hash.digest("hex").toLowerCase(); + if (got !== expected) { + updateDownloadState = "failed"; + console.warn(`[update] SHA-256 mismatch: got ${got}, want ${expected} — refusing to arm install`); + try { fs.unlinkSync(savedPath); } catch {} + emitUpdateAvailable(); + return; + } + updateDownloadPath = savedPath; + updateDownloadState = "ready"; + console.log(`[update] silent fetch complete + verified: ${savedPath}`); + emitUpdateAvailable(); + }); }); return; } diff --git a/package.json b/package.json index e152868c..3e0a0dfe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "theseus-navigator", - "version": "0.3.32", + "version": "0.3.33", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "author": "Silent Mode", "main": "main.js",