From dff14ec3e8bd78c83a344afe1c2deb59cd23e59d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Bene=C5=A1?= Date: Sat, 25 Jul 2026 01:24:35 +0200 Subject: [PATCH] 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 --- ytsage/core/ytsage_ffmpeg.py | 9 +++++++-- ytsage/core/ytsage_utils.py | 21 ++++++++++++++------- ytsage/core/ytsage_yt_dlp.py | 19 ++++++++++++------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/ytsage/core/ytsage_ffmpeg.py b/ytsage/core/ytsage_ffmpeg.py index c2386b3..51842cb 100644 --- a/ytsage/core/ytsage_ffmpeg.py +++ b/ytsage/core/ytsage_ffmpeg.py @@ -282,9 +282,14 @@ def install_ffmpeg_windows(progress_callback=None) -> bool: if progress_callback: progress_callback("🔐 Verifying download integrity...") 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: - 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...") if progress_callback: diff --git a/ytsage/core/ytsage_utils.py b/ytsage/core/ytsage_utils.py index 78a096f..52bdd5e 100644 --- a/ytsage/core/ytsage_utils.py +++ b/ytsage/core/ytsage_utils.py @@ -463,7 +463,7 @@ def update_yt_dlp() -> bool: # Download the latest version try: - response = requests.get(YTDLP_DOWNLOAD_URL, stream=True) + response = requests.get(YTDLP_DOWNLOAD_URL, stream=True, timeout=60) if response.status_code == 200: # Create a temporary file 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): 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 if OS_NAME != "Windows": 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: - # On Windows, we need to remove the old file first - if OS_NAME == "Windows" and yt_dlp_path.exists(): - yt_dlp_path.unlink(missing_ok=True) - - Path(temp_file).rename(yt_dlp_path) + os.replace(temp_file, yt_dlp_path) logger.info("yt-dlp binary successfully updated") return True except Exception as e: logger.exception(f"Error replacing yt-dlp binary: {e}") + Path(temp_file).unlink(missing_ok=True) return False else: logger.info(f"Failed to download latest yt-dlp: HTTP {response.status_code}") diff --git a/ytsage/core/ytsage_yt_dlp.py b/ytsage/core/ytsage_yt_dlp.py index 62eb4ec..4eceebb 100644 --- a/ytsage/core/ytsage_yt_dlp.py +++ b/ytsage/core/ytsage_yt_dlp.py @@ -112,6 +112,10 @@ class DownloadYtdlpThread(QThread): try: # Extra logic moved to src\utils\ytsage_constants.py 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 logger.info(f"Downloading yt-dlp from: {YTDLP_DOWNLOAD_URL}") @@ -123,7 +127,7 @@ class DownloadYtdlpThread(QThread): if total_size == 0: self.progress_signal.emit(100) - with open(exe_path, "wb") as f: + with open(part_path, "wb") as f: downloaded = 0 for data in response.iter_content(block_size): f.write(data) @@ -133,22 +137,23 @@ class DownloadYtdlpThread(QThread): self.progress_signal.emit(progress) logger.info("Download complete, verifying 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 logger.error("SHA256 verification failed! Removing downloaded file.") - if Path(exe_path).exists(): - Path(exe_path).unlink() + part_path.unlink(missing_ok=True) self.finished_signal.emit( - False, + False, "SHA256 verification failed. The downloaded file may be corrupted or tampered with." ) return # Make executable on macOS and Linux 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!") self.finished_signal.emit(True, str(exe_path))