Harden app updater: per-hop redirect pinning, truncation + path-guard fixes

Review follow-ups on the in-app updater:

- Download via Electron net.request with redirect:'manual', re-validating the
  host on EVERY hop. fetch() followed redirects automatically and undici hides
  the Location header, so the trusted-host pin only covered the first URL — a
  302 from the trusted host could bounce the download to an arbitrary server.
- Reject truncated downloads (received !== Content-Length) and an 'aborted'
  mid-stream so a partial installer is never launched; clean up the temp file
  on any failure.
- runAppUpdate: compare the parent dir by equality instead of startsWith(),
  which a sibling like `<temp>_evil\x.exe` defeated.
- checkForAppUpdate: 15s AbortController timeout (mirrors the yt-dlp calls) and
  pin htmlUrl to the trusted host before handing it to the OS browser.
- parseVersion: ignore -prerelease/+build suffixes so a prerelease never sorts
  above its final release.
- SettingsView: funnel a failed check into the single error slot so two error
  messages can't render at once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 05:06:01 -04:00
parent 981d2dd34d
commit 7134a3d634
2 changed files with 156 additions and 55 deletions
+5 -6
View File
@@ -256,7 +256,11 @@ export function SettingsView(): React.JSX.Element {
setAppUpd(null)
setAppUpdError(null)
try {
setAppUpd(await window.api.checkForAppUpdate())
const info = await window.api.checkForAppUpdate()
// Funnel a failed check into the single error slot rather than storing a
// second not-ok AppUpdateInfo, so only one error message can ever render.
if (info.ok) setAppUpd(info)
else setAppUpdError(info.error ?? 'Update check failed.')
} catch (e) {
setAppUpdError(e instanceof Error ? e.message : String(e))
} finally {
@@ -883,11 +887,6 @@ export function SettingsView(): React.JSX.Element {
<Text>You&apos;re up to date v{appUpd.currentVersion} is the latest.</Text>
)}
{appUpd && !appUpd.ok && (
<Caption1 style={{ color: tokens.colorPaletteRedForeground1, whiteSpace: 'pre-wrap' }}>
{appUpd.error}
</Caption1>
)}
{appUpdError && (
<Caption1 style={{ color: tokens.colorPaletteRedForeground1, whiteSpace: 'pre-wrap' }}>
{appUpdError}