|
| 1 | +From 5d21b0f9ba9d397f45bb9003635be81df846f894 Mon Sep 17 00:00:00 2001 |
| 2 | +From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= <alex.gronholm@nextday.fi> |
| 3 | +Date: Thu, 22 Jan 2026 01:41:14 +0200 |
| 4 | +Subject: [PATCH] Fixed security issue around wheel unpack (#675) |
| 5 | + |
| 6 | +A maliciously crafted wheel could cause the permissions of a file outside the unpack tree to be altered. |
| 7 | + |
| 8 | +Fixes CVE-2026-24049. |
| 9 | + |
| 10 | +Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> |
| 11 | +Upstream-reference: https://github.com/pypa/wheel/commit/7a7d2de96b22a9adf9208afcc9547e1001569fef.patch |
| 12 | +--- |
| 13 | + src/wheel/cli/unpack.py | 4 ++-- |
| 14 | + tests/cli/test_unpack.py | 23 +++++++++++++++++++++++ |
| 15 | + 2 files changed, 25 insertions(+), 2 deletions(-) |
| 16 | + |
| 17 | +diff --git a/src/wheel/cli/unpack.py b/src/wheel/cli/unpack.py |
| 18 | +index d48840e..83dc742 100644 |
| 19 | +--- a/src/wheel/cli/unpack.py |
| 20 | ++++ b/src/wheel/cli/unpack.py |
| 21 | +@@ -19,12 +19,12 @@ def unpack(path: str, dest: str = ".") -> None: |
| 22 | + destination = Path(dest) / namever |
| 23 | + print(f"Unpacking to: {destination}...", end="", flush=True) |
| 24 | + for zinfo in wf.filelist: |
| 25 | +- wf.extract(zinfo, destination) |
| 26 | ++ target_path = Path(wf.extract(zinfo, destination)) |
| 27 | + |
| 28 | + # Set permissions to the same values as they were set in the archive |
| 29 | + # We have to do this manually due to |
| 30 | + # https://github.com/python/cpython/issues/59999 |
| 31 | + permissions = zinfo.external_attr >> 16 & 0o777 |
| 32 | +- destination.joinpath(zinfo.filename).chmod(permissions) |
| 33 | ++ target_path.chmod(permissions) |
| 34 | + |
| 35 | + print("OK") |
| 36 | +diff --git a/tests/cli/test_unpack.py b/tests/cli/test_unpack.py |
| 37 | +index ae584af..75fe193 100644 |
| 38 | +--- a/tests/cli/test_unpack.py |
| 39 | ++++ b/tests/cli/test_unpack.py |
| 40 | +@@ -34,3 +34,26 @@ def test_unpack_executable_bit(tmp_path): |
| 41 | + unpack(str(wheel_path), str(tmp_path)) |
| 42 | + assert not script_path.is_dir() |
| 43 | + assert stat.S_IMODE(script_path.stat().st_mode) == 0o755 |
| 44 | ++ |
| 45 | ++ |
| 46 | ++@pytest.mark.skipif( |
| 47 | ++ platform.system() == "Windows", reason="Windows does not support chmod()" |
| 48 | ++) |
| 49 | ++def test_chmod_outside_unpack_tree(tmp_path_factory: TempPathFactory) -> None: |
| 50 | ++ wheel_path = tmp_path_factory.mktemp("build") / "test-1.0-py3-none-any.whl" |
| 51 | ++ with WheelFile(wheel_path, "w") as wf: |
| 52 | ++ wf.writestr( |
| 53 | ++ "test-1.0.dist-info/METADATA", |
| 54 | ++ "Metadata-Version: 2.4\nName: test\nVersion: 1.0\n", |
| 55 | ++ ) |
| 56 | ++ wf.writestr("../../system-file", b"malicious data") |
| 57 | ++ |
| 58 | ++ extract_root_path = tmp_path_factory.mktemp("extract") |
| 59 | ++ system_file = extract_root_path / "system-file" |
| 60 | ++ extract_path = extract_root_path / "subdir" |
| 61 | ++ system_file.write_bytes(b"important data") |
| 62 | ++ system_file.chmod(0o755) |
| 63 | ++ unpack(str(wheel_path), str(extract_path)) |
| 64 | ++ |
| 65 | ++ assert system_file.read_bytes() == b"important data" |
| 66 | ++ assert stat.S_IMODE(system_file.stat().st_mode) == 0o755 |
| 67 | +-- |
| 68 | +2.45.4 |
| 69 | + |
0 commit comments