VYPR
High severityOSV Advisory· Published Jan 22, 2026· Updated Jan 27, 2026

wheel Allows Arbitrary File Permission Modification via Path Traversal

CVE-2026-24049

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.

PackageAffected versionsPatched versions
wheelPyPI
>= 0.40.0, < 0.46.20.46.2

Affected products

1
  • Range: 0.40.0, 0.41.0, 0.41.1, …

Patches

2
7a7d2de96b22

Fixed security issue around wheel unpack (#675)

https://github.com/pypa/wheelAlex GrönholmJan 21, 2026via ghsa
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
    
934fe177ff91

Changed `wheel unpack` to honor the original permissions of files (#514)

https://github.com/pypa/wheelAlex GrönholmMar 13, 2023via ghsa
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

News mentions

0

No linked articles in our index yet.