8000 Security: harden the auto-update channel (no authenticity, no quarantine window) · Issue #29 · amirlehmam/wmux · GitHub
[go: up one dir, main page]

Skip to content

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

Closed

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.ts — autoUpdater.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-178 — latest.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

Activity

  1. kevmtt commented on Jun 8, 2026

    @kevmtt
    ContributorAuthor

    With the code parts I can help and contribute. But first I would like to know more thoughts.

  2. amirlehmam commented on Jun 8, 2026

    @amirlehmam
    Owner

    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.ts no longer auto-downloads/installs a release until it has been public for N days (default 3, configurable via WMUX_MIN_RELEASE_AGE_DAYS; 0 disables for an expedited security fix). Age is read from GitHub's server-side published_at (not the attacker-writable latest.yml releaseDate). checkForUpdates re-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 — autoDownload and autoInstallOnAppQuit are now both false; 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.ts comment (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:

    • (2) Independent signature on the update payload, verified against a build-embedded public key (minisign/cosign/Sigstore)
    • (3) Re-enable Authenticode signing + publisherName pinning (blocked on SignPath OSS quota)
    • (5) Build provenance/attestation, least-privilege GITHUB_TOKEN, protected v* tags, staged rollout

    Quarantine + user-confirmed install ✅ shipped; signature/provenance tracked above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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

      0