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:
@@ -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:
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -133,22 +137,23 @@ class DownloadYtdlpThread(QThread):
|
|||||||
self.progress_signal.emit(progress)
|
self.progress_signal.emit(progress)
|
||||||
|
|
||||||
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."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# 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))
|
||||||
|
|||||||
Reference in New Issue
Block a user