wheel Allows Arbitrary File Permission Modification via Path Traversal
Description
wheel is a command line tool for manipulating Python wheel files, as defined in PEP 427. In versions 0.40.0 through 0.46.1, the unpack function is vulnerable to file permission modification through mishandling of file permissions after extraction. The logic blindly trusts the filename from the archive header for the chmod operation, even though the extraction process itself might have sanitized the path. Attackers can craft a malicious wheel file that, when unpacked, changes the permissions of critical system files (e.g., /etc/passwd, SSH keys, config files), allowing for Privilege Escalation or arbitrary code execution by modifying now-writable scripts. This issue has been fixed in version 0.46.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
wheelPyPI | >= 0.40.0, < 0.46.2 | 0.46.2 |
Affected products
1Patches
27a7d2de96b22Fixed security issue around wheel unpack (#675)
3 files changed · +27 −2
docs/news.rst+2 −0 modified@@ -7,6 +7,8 @@ Release Notes v70.1 - Importing ``wheel.bdist_wheel`` now emits a ``FutureWarning`` instead of a ``DeprecationWarning`` +- Fixed ``wheel unpack`` potentially altering the permissions of files outside of the + destination tree with maliciously crafted wheels (CVE-2026-24049) **0.46.1 (2025-04-08)**
src/wheel/_commands/unpack.py+2 −2 modified@@ -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")
tests/commands/test_unpack.py+23 −0 modified@@ -54,3 +54,26 @@ def test_unpack_executable_bit(tmp_path: Path) -> None: run_command("unpack", "--dest", tmp_path, wheel_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) + run_command("unpack", "--dest", extract_path, wheel_path) + + assert system_file.read_bytes() == b"important data" + assert stat.S_IMODE(system_file.stat().st_mode) == 0o755
934fe177ff91Changed `wheel unpack` to honor the original permissions of files (#514)
3 files changed · +33 −1
docs/news.rst+1 −0 modified@@ -4,6 +4,7 @@ Release Notes **UNRELEASED** - Updated vendored ``packaging`` to 23.0 +- ``wheel unpack`` now preserves the executable attribute of extracted files - Fixed spaces in platform names not being converted to underscores (PR by David Tucker) - Fixed ``RECORD`` files in generated wheels missing the regular file attribute - Fixed ``DeprecationWarning`` about the use of the deprecated ``pkg_resources`` API
src/wheel/cli/unpack.py+8 −1 modified@@ -18,6 +18,13 @@ def unpack(path: str, dest: str = ".") -> None: namever = wf.parsed_filename.group("namever") destination = Path(dest) / namever print(f"Unpacking to: {destination}...", end="", flush=True) - wf.extractall(destination) + for zinfo in wf.filelist: + 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) print("OK")
tests/cli/test_unpack.py+24 −0 modified@@ -1,6 +1,12 @@ from __future__ import annotations +import platform +import stat + +import pytest + from wheel.cli.unpack import unpack +from wheel.wheelfile import WheelFile def test_unpack(wheel_paths, tmp_path): @@ -10,3 +16,21 @@ def test_unpack(wheel_paths, tmp_path): """ for wheel_path in wheel_paths: unpack(wheel_path, str(tmp_path)) + + +@pytest.mark.skipif( + platform.system() == "Windows", reason="Windows does not support the executable bit" +) +def test_unpack_executable_bit(tmp_path): + wheel_path = tmp_path / "test-1.0-py3-none-any.whl" + script_path = tmp_path / "script" + script_path.write_bytes(b"test script") + script_path.chmod(0o755) + with WheelFile(wheel_path, "w") as wf: + wf.write(str(script_path), "nested/script") + + script_path.unlink() + script_path = tmp_path / "test-1.0" / "nested" / "script" + unpack(str(wheel_path), str(tmp_path)) + assert not script_path.is_dir() + assert stat.S_IMODE(script_path.stat().st_mode) == 0o755
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-8rrh-rw8j-w5fxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-24049ghsaADVISORY
- github.com/pypa/wheel/commit/7a7d2de96b22a9adf9208afcc9547e1001569fefghsax_refsource_MISCWEB
- github.com/pypa/wheel/commit/934fe177ff912c8e03d5ae951d3805e1fd90ba5eghsaWEB
- github.com/pypa/wheel/releases/tag/0.46.2ghsax_refsource_MISCWEB
- github.com/pypa/wheel/security/advisories/GHSA-8rrh-rw8j-w5fxghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.