From 24ab89cdacfa68857b4c7b5fb5234a75193dc941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 22 Jan 2026 01:41:14 +0200 Subject: [PATCH] Fixed security issue around wheel unpack (#675) A maliciously crafted wheel could cause the permissions of a file outside the unpack tree to be altered. Fixes CVE-2026-24049. (cherry picked from commit 7a7d2de96b22a9adf9208afcc9547e1001569fef) --- docs/news.rst | 5 +++++ src/wheel/cli/unpack.py | 4 ++-- tests/cli/test_unpack.py | 23 +++++++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/news.rst b/docs/news.rst index 9e5488a..1033520 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -1,6 +1,11 @@ Release Notes ============= +**BACKPORTS** + +- Fixed ``wheel unpack`` potentially altering the permissions of files outside of the + destination tree with maliciously crafted wheels (CVE-2026-24049) + **0.41.2 (2023-08-22)** - Fixed platform tag detection for GraalPy and 32-bit python running on an aarch64 diff --git a/src/wheel/cli/unpack.py b/src/wheel/cli/unpack.py index d48840e..83dc742 100644 --- a/src/wheel/cli/unpack.py +++ b/src/wheel/cli/unpack.py @@ -19,12 +19,12 @@ def unpack(path: str, dest: str = ".") -> None: destination = Path(dest) / namever print(f"Unpacking to: {destination}...", end="", flush=True) for zinfo in wf.filelist: - wf.extract(zinfo, destination) + target_path = Path(wf.extract(zinfo, destination)) # Set permissions to the same values as they were set in the archive # We have to do this manually due to # https://github.com/python/cpython/issues/59999 permissions = zinfo.external_attr >> 16 & 0o777 - destination.joinpath(zinfo.filename).chmod(permissions) + target_path.chmod(permissions) print("OK") diff --git a/tests/cli/test_unpack.py b/tests/cli/test_unpack.py index ae584af..951ba29 100644 --- a/tests/cli/test_unpack.py +++ b/tests/cli/test_unpack.py @@ -34,3 +34,26 @@ def test_unpack_executable_bit(tmp_path): unpack(str(wheel_path), str(tmp_path)) assert not script_path.is_dir() assert stat.S_IMODE(script_path.stat().st_mode) == 0o755 + + +@pytest.mark.skipif( + platform.system() == "Windows", reason="Windows does not support chmod()" +) +def test_chmod_outside_unpack_tree(tmp_path_factory: TempPathFactory) -> None: + wheel_path = tmp_path_factory.mktemp("build") / "test-1.0-py3-none-any.whl" + with WheelFile(wheel_path, "w") as wf: + wf.writestr( + "test-1.0.dist-info/METADATA", + "Metadata-Version: 2.4\nName: test\nVersion: 1.0\n", + ) + wf.writestr("../../system-file", b"malicious data") + + extract_root_path = tmp_path_factory.mktemp("extract") + system_file = extract_root_path / "system-file" + extract_path = extract_root_path / "subdir" + system_file.write_bytes(b"important data") + system_file.chmod(0o755) + unpack(str(wheel_path), str(extract_path)) + + assert system_file.read_bytes() == b"important data" + assert stat.S_IMODE(system_file.stat().st_mode) == 0o755 -- 2.52.0