Skip to content

Security: harden the auto-update channel (no authenticity, no quarantine window) #29

@kevmtt

Description

@kevmtt

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.tsautoUpdater.autoDownload = true, autoUpdater.autoInstallOnAppQuit = true, no publisherName / signature checks.
  • electron-builder.json:7-11 — update feed = GitHub releases of amirlehmam/wmux.
  • .github/workflows/release.yml:58-119 — Authenticode signing via SignPath is commented out → shipped wmux.exe is unsigned.
  • .github/workflows/release.yml:152-178latest.yml is just sha512(zip) generated in the same job and uploaded to the same release (integrity, not authenticity).
  • src/main/index.ts:200-203 — both initAutoUpdater() (electron-updater) and initUpdateChecker() (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-age mitigation 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, protected v* tags, required 2FA on the repo.
  • Pin/lock build-time deps and consider an npm minimum-release-age for
    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 use electron-updater … no latest.yml") is stale - release.yml now emits latest.yml and updater.ts is 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_TOKEN scope minimized.
  • update-checker.ts comment corrected.

References

  • electron-updater code-signing & publisherName verification docs
  • Sigstore / minisign for offline artifact signing
  • npm minimum-release-age (supply-chain cooldown) prior art

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions