Repository navigation
Description
Security: harden the auto-update channel (no authenticity, no quarantine window)
Summary
wmux auto-downloads and silently installs updates from GitHub Releases with no authenticity verification and no release-age safeguard. The only integrity check is a SHA-512 that ships in the same release as the binary it describes, so
it proves the bytes weren't corrupted — not who produced them. Combined with autoInstallOnAppQuit = true, any actor who can publish a release to amirlehmam/wmux (leaked CI token, compromised maintainer account, poisoned build-time dependency, or GitHub-side tampering) gets silent RCE on every installed machine, with near-instant fan-out.
Given the recent wave of npm/supply-chain source-injection attacks, this is the highest-leverage trust dependency in the project.
Where it lives
src/main/updater.ts—autoUpdater.autoDownload = true,autoUpdater.autoInstallOnAppQuit = true, nopublisherName/ signature checks.electron-builder.json:7-11— update feed = GitHub releases ofamirlehmam/wmux..github/workflows/release.yml:58-119— Authenticode signing via SignPath is commented out → shippedwmux.exeis unsigned..github/workflows/release.yml:152-178—latest.ymlis justsha512(zip)generated in the same job and uploaded to the same release (integrity, not authenticity).src/main/index.ts:200-203— bothinitAutoUpdater()(electron-updater) andinitUpdateChecker()(notify-only) run when packaged.
Threat model
| Attacker capability | Today's outcome | Why current controls don't stop it |
|---|---|---|
Leak/abuse GITHUB_TOKEN in CI |
Publish malicious release | SHA-512 is attacker-generated alongside the binary |
| Compromise maintainer GitHub account | Replace release assets | No signature; unsigned exe → no publisherName check |
Poison a build-time dep (npm ci) |
Inject code at build, ships "legit" | CI produces the hash over already-poisoned bytes |
| Tamper with release assets server-side | Swap zip + latest.yml |
Both fetched from the same untrusted origin |
In every row, autoInstallOnAppQuit = true + 6h polling means the payload reaches all installs and self-installs on next quit, before anyone can react.
What's actually fine (so we don't over-rotate)
- electron-updater does verify the download against the SHA-512, so passive CDN corruption / partial-download tampering is caught.
- The update check path (
update-checker.ts) is notify-only and harmless. - No telemetry/exfiltration involved; this is purely about the install channel.
The gap is authenticity and propagation speed, not transport integrity.
Proposed fixes (in priority order)
1. Minimum release age (quarantine window) — cheap, high value, do first Do not auto-download/install a release until it has been public for N days (suggest 3–7, configurable). Implement by reading published_at from the GitHub release API and gating electron-updater accordingly; only call checkForUpdates() / allow download when now - published_at >= MIN_RELEASE_AGE.
- Denies attackers the instant fan-out that makes these attacks lethal.
- Gives the maintainer/community time to detect and yank a bad release before
clients adopt it. - Mirrors the npm
minimum-release-agemitigation now widely recommended for the
same class of supply-chain attacks. - Security/critical fixes can opt out via an explicit allowlist or a signed
"expedite" flag if ever needed.
2. Independent signature on the update metadata — real authenticity fix Sign the release artifact (or latest.yml) with an offline key whose public key is baked into the app at build time (e.g. minisign / cosign / Sigstore).
Verify the signature in updater.ts before allowing install. This decouples authenticity from "whoever can push to the GitHub release" — a release-channel compromise alone no longer yields a valid update.
3. Code signing (Authenticode) + publisher pinning Re-enable the SignPath steps (release.yml:58-119) and set electron-updater's publisherName so updates must be signed by the known publisher. Also fixes SmartScreen friction. (Tracked dependency: SignPath OSS quota.)
4. Make install user-confirmed, not silent
Replace autoInstallOnAppQuit = true with a prompt: surface "vX downloaded review release notes" and require an explicit click. Removes the silent-propagation property even if 1–3 are delayed.
5. Harden the supply chain around the build
- Build provenance / attestation (
actions/attest-build-provenance, SLSA). - Least-privilege
GITHUB_TOKEN, protectedv*tags, required 2FA on the repo. - Pin/lock build-time deps and consider an npm
minimum-release-agefor
dependencies too (defends row 3 of the threat model). - Staged/percentage rollout so a bad release reaches few users first.
6. Housekeeping
- The comment in
update-checker.ts:5-7("can't A540 use electron-updater … no latest.yml") is stale -release.ymlnow emitslatest.ymlandupdater.tsis active. Clarify the two update paths to avoid confusion.
Suggested acceptance criteria
- Updates are not installed until a release is ≥ N days old (configurable).
- Update payload signature verified against a build-embedded public key.
- Auto-install requires user confirmation (or is gated behind 1+2).
- Authenticode signing re-enabled (or tracked as a separate blocker).
- Release pipeline emits build provenance;
GITHUB_TOKENscope minimized. -
update-checker.tscomment corrected.
References
- electron-updater code-signing &
publisherNameverification docs - Sigstore / minisign for offline artifact signing
- npm
minimum-release-age(supply-chain cooldown) prior art
Activity
kevmtt commentedon Jun 8, 2026
With the code parts I can help and contribute. But first I would like to know more thoughts.
fix(browser,security): resolve issues #27, #28, #29
amirlehmam commentedon Jun 8, 2026
Thanks for this @kevmtt — excellent threat-model breakdown. The two highest-leverage, code-only mitigations you flagged as "do first" shipped in v0.8.3:
1. Quarantine window (min release age) —
updater.tsno longer auto-downloads/installs a release until it has been public for N days (default 3, configurable viaWMUX_MIN_RELEASE_AGE_DAYS;0disables for an expedited security fix). Age is read from GitHub's server-sidepublished_at(not the attacker-writablelatest.ymlreleaseDate).checkForUpdatesre-runs every 6h so a held release installs once it ages past the window — no restart needed. Unknown/unconfirmable age is treated conservatively (held, not installed).4. No silent install —
autoDownloadandautoInstallOnAppQuitare now bothfalse; installing requires an explicit confirmation dialog ("Install and restart" / "Later"). The instant silent-propagation property is gone even before signing lands.Also fixed the stale
update-checker.tscomment (item 6) — it now documents both update paths accurately.Leaving this open to track the remaining authenticity/supply-chain work, which needs keys/CI changes rather than just code:
publisherNamepinning (blocked on SignPath OSS quota)GITHUB_TOKEN, protectedv*tags, staged rolloutQuarantine + user-confirmed install ✅ shipped; signature/provenance tracked above.