Enforce SHA256 verification on every binary install path

Three gaps allowed an unverified binary to reach a trusted location:

- The ffmpeg ZIP fallback logged a warning on checksum mismatch and
  installed anyway (the 7z path already aborted). Abort instead.
- The yt-dlp auto-update path downloaded and renamed the binary over
  the verified one with no checksum at all. Verify against the official
  SHA2-256SUMS like the first-install path, and use atomic os.replace.
- The yt-dlp first install streamed the download directly to the
  trusted path and only verified afterwards; a crash in between left an
  unverified executable to be run on next launch. Download to .part and
  os.replace only after verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-07-25 01:24:35 +02:00
parent b40deb0d5f
commit dff14ec3e8
3 changed files with 33 additions and 16 deletions
+7 -2
View File
@@ -282,9 +282,14 @@ def install_ffmpeg_windows(progress_callback=None) -> bool:
if progress_callback: if progress_callback:
progress_callback("🔐 Verifying download integrity...") progress_callback("🔐 Verifying download integrity...")
if not verify_sha256(temp_file, FFMPEG_ZIP_SHA256_URL): if not verify_sha256(temp_file, FFMPEG_ZIP_SHA256_URL):
logger.warning("SHA-256 verification failed for zip file, proceeding anyway...") logger.error("SHA-256 verification failed for zip file, aborting installation")
if progress_callback: if progress_callback:
progress_callback("⚠️ SHA-256 verification failed, proceeding anyway...") progress_callback(" SHA-256 verification failed, aborting installation")
try:
Path(temp_file).unlink(missing_ok=True)
except Exception:
pass
return False
logger.info("Extracting FFmpeg components from zip archive...") logger.info("Extracting FFmpeg components from zip archive...")
if progress_callback: if progress_callback:
+14 -7
View File
@@ -463,7 +463,7 @@ def update_yt_dlp() -> bool:
# Download the latest version # Download the latest version
try: try:
response = requests.get(YTDLP_DOWNLOAD_URL, stream=True) response = requests.get(YTDLP_DOWNLOAD_URL, stream=True, timeout=60)
if response.status_code == 200: if response.status_code == 200:
# Create a temporary file # Create a temporary file
temp_file = f"{yt_dlp_path}.new" temp_file = f"{yt_dlp_path}.new"
@@ -472,21 +472,28 @@ def update_yt_dlp() -> bool:
for chunk in response.iter_content(chunk_size=8192): for chunk in response.iter_content(chunk_size=8192):
f.write(chunk) f.write(chunk)
# Verify against the official checksums before touching the
# trusted binary (same check as the first-install path)
from ytsage.core.ytsage_yt_dlp import verify_ytdlp_sha256
if not verify_ytdlp_sha256(Path(temp_file), YTDLP_DOWNLOAD_URL):
logger.error("SHA256 verification failed for yt-dlp update, keeping current binary")
Path(temp_file).unlink(missing_ok=True)
return False
# Make executable on Unix systems # Make executable on Unix systems
if OS_NAME != "Windows": if OS_NAME != "Windows":
os.chmod(temp_file, 0o755) os.chmod(temp_file, 0o755)
# Replace the old file with the new one # Replace the old file with the new one (os.replace is atomic
# and overwrites on Windows too)
try: try:
# On Windows, we need to remove the old file first os.replace(temp_file, yt_dlp_path)
if OS_NAME == "Windows" and yt_dlp_path.exists():
yt_dlp_path.unlink(missing_ok=True)
Path(temp_file).rename(yt_dlp_path)
logger.info("yt-dlp binary successfully updated") logger.info("yt-dlp binary successfully updated")
return True return True
except Exception as e: except Exception as e:
logger.exception(f"Error replacing yt-dlp binary: {e}") logger.exception(f"Error replacing yt-dlp binary: {e}")
Path(temp_file).unlink(missing_ok=True)
return False return False
else: else:
logger.info(f"Failed to download latest yt-dlp: HTTP {response.status_code}") logger.info(f"Failed to download latest yt-dlp: HTTP {response.status_code}")
+10 -5
View File
@@ -112,6 +112,10 @@ class DownloadYtdlpThread(QThread):
try: try:
# Extra logic moved to src\utils\ytsage_constants.py # Extra logic moved to src\utils\ytsage_constants.py
exe_path = YTDLP_APP_BIN_PATH exe_path = YTDLP_APP_BIN_PATH
# Download to a temp name and only move to the trusted path after
# the hash checks out, so a crash mid-download can never leave an
# unverified binary where the app will execute it.
part_path = exe_path.with_name(exe_path.name + ".part")
# Download with progress reporting # Download with progress reporting
logger.info(f"Downloading yt-dlp from: {YTDLP_DOWNLOAD_URL}") logger.info(f"Downloading yt-dlp from: {YTDLP_DOWNLOAD_URL}")
@@ -123,7 +127,7 @@ class DownloadYtdlpThread(QThread):
if total_size == 0: if total_size == 0:
self.progress_signal.emit(100) self.progress_signal.emit(100)
with open(exe_path, "wb") as f: with open(part_path, "wb") as f:
downloaded = 0 downloaded = 0
for data in response.iter_content(block_size): for data in response.iter_content(block_size):
f.write(data) f.write(data)
@@ -135,11 +139,10 @@ class DownloadYtdlpThread(QThread):
logger.info("Download complete, verifying SHA256 hash...") logger.info("Download complete, verifying SHA256 hash...")
# Verify SHA256 hash # Verify SHA256 hash
if not verify_ytdlp_sha256(exe_path, YTDLP_DOWNLOAD_URL): if not verify_ytdlp_sha256(part_path, YTDLP_DOWNLOAD_URL):
# Hash verification failed - delete the downloaded file # Hash verification failed - delete the downloaded file
logger.error("SHA256 verification failed! Removing downloaded file.") logger.error("SHA256 verification failed! Removing downloaded file.")
if Path(exe_path).exists(): part_path.unlink(missing_ok=True)
Path(exe_path).unlink()
self.finished_signal.emit( self.finished_signal.emit(
False, False,
"SHA256 verification failed. The downloaded file may be corrupted or tampered with." "SHA256 verification failed. The downloaded file may be corrupted or tampered with."
@@ -148,7 +151,9 @@ class DownloadYtdlpThread(QThread):
# Make executable on macOS and Linux # Make executable on macOS and Linux
if OS_NAME != "Windows": if OS_NAME != "Windows":
os.chmod(exe_path, 0o755) os.chmod(part_path, 0o755)
os.replace(part_path, exe_path)
logger.info("yt-dlp downloaded and verified successfully!") logger.info("yt-dlp downloaded and verified successfully!")
self.finished_signal.emit(True, str(exe_path)) self.finished_signal.emit(True, str(exe_path))