mirror of
https://github.com/usestrix/strix.git
synced 2026-10-29 06:32:26 +00:00
security: harden self-update against supply-chain and path-traversal attacks
Three related hardening measures for _download_and_replace(): 1. REFUSE UNVERIFIED BINARIES (was: silent skip) When no SHA-256 digest is published for a release asset, the previous code printed a dim warning and proceeded to replace the running binary with unverified content. If an attacker compromises the GitHub release (account takeover, Actions supply-chain, CDN MitM on a misconfigured proxy), they can inject a malicious binary that is installed without any integrity check. Now: raise RuntimeError so the update is aborted. The operator can still install manually after independent verification. 2. ZIP-SLIP GUARD (Windows path) zipfile.extract() uses the archive member name as-is. A crafted zip containing '../../malicious.exe' writes outside the staging directory. Python >=3.12 has partial mitigations, but Python 3.11 (in-support per pyproject.toml requires-python) is fully vulnerable. Now: resolve the extraction target and verify it stays inside tmp_dir before extracting. 3. TAR-SLIP GUARD + PYTHON 3.11 FALLBACK (Unix path) tarfile.extract(filter='data') was introduced in Python 3.12. On Python 3.11 it raises TypeError, causing the self-update to crash. Additionally, without the filter on 3.11, tar members with absolute paths or '..' components could escape. Now: validate the resolved path (like the zip guard), use filter='data' when available, and fall back gracefully on 3.11.
This commit is contained in:
parent
08126eb518
commit
50051fba8c
1 changed files with 27 additions and 3 deletions
|
|
@ -306,14 +306,38 @@ def _download_and_replace(version: str, target: str, console: Console) -> bool:
|
|||
f"expected sha256 {expected_digest}, got {actual_digest}"
|
||||
)
|
||||
else:
|
||||
console.print("[dim yellow]No published checksum available; skipping verification[/]")
|
||||
raise RuntimeError(
|
||||
f"No published checksum available for {filename}; "
|
||||
f"refusing to install an unverified binary. "
|
||||
f"Please ensure the release includes asset digests."
|
||||
)
|
||||
|
||||
if is_windows:
|
||||
with zipfile.ZipFile(archive_path) as zf:
|
||||
zf.extract(binary_name, tmp_dir)
|
||||
# Zip-slip guard: verify the member does not escape tmp_dir.
|
||||
member = zf.getinfo(binary_name)
|
||||
extracted = (tmp_dir / member.filename).resolve()
|
||||
if not str(extracted).startswith(str(tmp_dir.resolve())):
|
||||
raise RuntimeError(
|
||||
f"Zip member {member.filename!r} would escape "
|
||||
f"the staging directory (zip-slip)"
|
||||
)
|
||||
zf.extract(member, tmp_dir)
|
||||
else:
|
||||
with tarfile.open(archive_path, "r:gz") as tf:
|
||||
tf.extract(binary_name, tmp_dir, filter="data")
|
||||
# Tar-slip guard: verify the member does not escape tmp_dir.
|
||||
member = tf.getmember(binary_name)
|
||||
extracted = (tmp_dir / member.name).resolve()
|
||||
if not str(extracted).startswith(str(tmp_dir.resolve())):
|
||||
raise RuntimeError(
|
||||
f"Tar member {member.name!r} would escape "
|
||||
f"the staging directory (tar-slip)"
|
||||
)
|
||||
try:
|
||||
tf.extract(member, tmp_dir, filter="data")
|
||||
except TypeError:
|
||||
# Python <3.12 does not support the filter parameter.
|
||||
tf.extract(member, tmp_dir)
|
||||
|
||||
new_binary = tmp_dir / binary_name
|
||||
new_binary.chmod(new_binary.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue