CVE-2026-24049

Wheel of Misfortune: Arbitrary File Permission Modification in Python's Wheel

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 23, 2026·5 min read·5 visits

Executive Summary (TL;DR)

The `wheel unpack` command contains a vulnerability where it sanitizes file paths during extraction but uses the raw, unsanitized filename when applying file permissions (chmod). An attacker can craft a malicious wheel file that, when unpacked, changes the permissions of critical system files (like `/etc/shadow` or SSH keys) to be world-writable, leading to total system compromise.

A critical logic flaw in the `wheel` packaging tool allows attackers to modify file permissions on the host system via path traversal, potentially leading to privilege escalation.

The Hook: Boring Tools, Exciting Bugs

We often focus our security research on complex web applications or kernel drivers, ignoring the mundane plumbing that holds our ecosystems together. wheel is the reference implementation for Python's binary package format. It is the boring, reliable forklift of the Python world. It moves code from PyPI to your virtual environment.

But in the unpack command—a utility used to inspect wheel contents—developers left a logic gap wide enough to drive a truck through. It is not a memory corruption bug. It is not a complex race condition. It is a simple disagreement between two lines of code about where a file is actually located.

This vulnerability proves that even standard, mature tooling can harbor 'design implementation' flaws that turn a simple archive extraction into a privilege escalation primitive. If you use wheel to inspect untrusted packages in a CI/CD pipeline or on a workstation, you effectively gave that package the ability to chmod any file you own.

The Flaw: A Tale of Two Paths

The vulnerability stems from a disconnect between extraction and permission restoration. When wheel unpacks an archive, it iterates through the file list. For every file, it needs to do two things: write the data to disk, and set the correct file mode (permissions) as specified in the archive attributes.

Step one relies on Python's built-in zipfile module. The extract() method is paranoid; if it sees a filename like ../../etc/passwd, it sanitizes it. It strips the directory traversal characters and safely writes the file inside the target directory. The zipfile module says: "Nice try, hacker," and neutralizes the path.

But step two is where the logic falls apart. After extraction, the code manually applies permissions. To do this, it constructed the path to the file again. Instead of asking zipfile "Where did you put that file?", it went back to the raw, untrusted ZIP header. It took the original filename (including the ../../) and joined it to the destination path.

This creates a classic split-brain scenario: The content is written safely to destination/etc/passwd, but the permissions are applied to destination/../../etc/passwd (which resolves to /etc/passwd). The code thought it was locking the front door, but it was actually unlocking the bank vault down the street.

The Smoking Gun

Let's look at the code in src/wheel/_commands/unpack.py. This is a textbook example of why you should never trust input twice. Once you sanitize it, use the sanitized version.

Here is the vulnerable code block:

# VULNERABLE LOGIC
for zinfo in wf.filelist:
    # 1. Safe extraction (returns the sanitized path, but return value is IGNORED)
    wf.extract(zinfo, destination)
 
    # 2. Get permissions from the untrusted ZIP header
    permissions = zinfo.external_attr >> 16 & 0o777
 
    # 3. FATAL ERROR: Re-constructing path from untrusted zinfo.filename
    destination.joinpath(zinfo.filename).chmod(permissions)

Because pathlib's joinpath resolves traversal characters, zinfo.filename breaks out of destination. The fix, implemented in version 0.46.2, is elegantly simple: capture the return value of extract(), which tells us the actual absolute path where the file landed.

# FIXED LOGIC
for zinfo in wf.filelist:
    # 1. Capture the authoritative path
    target_path = Path(wf.extract(zinfo, destination))
    
    permissions = zinfo.external_attr >> 16 & 0o777
    
    # 2. Apply permissions to the authoritative path
    target_path.chmod(permissions)

The Exploit: Public Library Mode

Exploiting this requires creating a valid wheel file that lies about its contents. We don't need to overflow a buffer; we just need to edit some ZIP headers. The goal is to make sensitive system files world-writable.

The Attack Chain:

  1. Crafting: The attacker creates a wheel containing a dummy file. They manually edit the ZIP central directory to rename this file to ../../../../../../etc/shadow (or ~/.ssh/authorized_keys).
  2. Permissioning: The attacker sets the external_attr field in the ZIP header to represent 0o777 (rwxrwxrwx).
  3. Delivery: The attacker uploads this package to a repository or tricks a user/admin into inspecting it with wheel unpack malicious.whl.

The Execution: When the victim runs the command, the tool extracts the dummy content safely. But milliseconds later, it executes chmod(0o777, '/etc/shadow').

If the victim is running as root (common in Docker containers or bad CI setups), /etc/shadow becomes world-writable. The attacker—now just a lowly local user—can open the shadow file, delete the root password hash, or add their own user with UID 0. Game over.

The Fix & Mitigation

The remediation is straightforward: Update wheel immediately.

Run the following command in all your environments:

pip install --upgrade wheel

Verify you are on version 0.46.2 or higher.

If you cannot update immediately, stop using wheel unpack. If you need to inspect wheel contents, use standard unzip tools (unzip -l file.whl) or Python's zipfile module directly, as standard tools generally do not attempt to replicate permissions blindly across traversal paths.

[!NOTE] This vulnerability highlights the importance of running package management tools with the least privilege necessary. Never run pip or wheel as root unless absolutely required.

Fix Analysis (1)

Technical Appendix

CVSS Score
7.1/ 10
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H
EPSS Probability
0.02%
Top 95% most exploited

Affected Systems

Linux workstations using `wheel` CLICI/CD pipelines processing untrusted Python packagesDeveloper environments

Affected Versions Detail

Product
Affected Versions
Fixed Version
wheel
pypa
<= 0.46.10.46.2
AttributeDetail
CWE IDCWE-22 & CWE-732
Attack VectorLocal (User-Assisted)
CVSS v3.17.1 (High)
ImpactIntegrity & Availability
Exploit StatusPoC Available
PlatformPython (Cross-Platform)
CWE-22
Path Traversal

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') leading to Incorrect Permission Assignment for Critical Resource

Vulnerability Timeline

Fix committed to main branch
2026-01-21
Release 0.46.2 published
2026-01-22
GHSA-8rrh-rw8j-w5fx published
2026-01-22

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.