GHSA-xmj3-9gf7-h52w json
setuptools: Windows path escape via namespace_packages.txt during legacy wheel installation Description
published 2026-10-23 16 d ago modified 2026-10-23 16 d ago
- cvss
- —
- not scored
- cisa kev
- Not listed
- not in CISA's catalog
- epss
- —
- no score, EPSS covers CVEs only
- public exploits
- 0
- none known
Summary
### Summary
On Windows, attacker-controlled content in a wheel's `.dist-info/namespace_packages.txt` can cause setuptools' legacy wheel installation path to create an `__init__.py` outside the requested `destination_eggdir`.
The attacker can influence the destination path, but not the contents of the created file.
### Details
Tested setuptools commit:
`6ead555c5fb29bc57fe6105b1bffc163f56fd558`
Relevant source:
`setuptools/wheel.py`
SHA-256:
`359b7e0d5dd8c3135653abe08a8ed5dc0352eadfd7df22cb5e2b5303b7c3cafd`
The relevant call path is:
`Wheel(wheel_path).install_as_egg(destination)` → `_install_as_egg` → `_convert_metadata` → `_fix_namespace_packages`
A wheel can provide attacker-controlled content in:
`<distribution>.dist-info/namespace_packages.txt`
The namespace value is eventually used approximately as:
```python mod_dir = os.path.join(destination_eggdir, *mod.split('.')) mod_init = os.path.join(mod_dir, '__init__.py')
If mod is a Windows drive-qualified absolute path containing no dots, mod.split('.') preserves it as a single component. On Windows, os.path.join(destination_eggdir, absolute_component) discards the original base directory and resolves to the attacker-supplied absolute path. The existing wheel filename and ZIP traversal checks do not validate this metadata value before it is subsequently interpreted as a filesystem path.
```markdown ### PoC
The reproduction was performed entirely inside a disposable temporary directory.
Layout:
```text sandbox/ destination/ outside/ input/ canary.txt ```
A synthetic wheel was created containing:
`demo_pkg-1.0.dist-info/namespace_packages.txt`
whose content was the absolute path of:
`sandbox\outside`
The real acquired setuptools implementation was then invoked:
```python setuptools.wheel.Wheel(synthetic_wheel).install_as_egg( sandbox_destination ) ```
The operation completed without exception.
Expected metadata was created under:
`sandbox\destination\EGG-INFO`
but setuptools also created:
`sandbox\outside\__init__.py`
The file did not exist before execution.
Its contents were:
```python __import__('pkg_resources').declare_namespace(__name__) ```
The unrelated canary remained byte-identical.
The sandbox was deleted after recording the evidence. ```
### Impact
A crafted wheel can cause setuptools to create a fixed-content `__init__.py` outside the intended installation directory at a filesystem path influenced by the wheel, with the permissions of the process performing the installation.
The demonstrated reproduction required the target parent directory to already exist.
Patch / fix
- pypi setuptools 84.1.0
- github.com/pypa/setuptools@6d07b8d commit
- github.com/pypa/setuptools#5329 pull request
- github.com/pypa/setuptools/releases/tag/v84.1.0 release
Affected packages
| ecosystem | package | affected | fixed in |
|---|---|---|---|
| pypi | setuptools | >=38.2.2, <84.1.0 |
84.1.0 |
Classification
- Weakness
-
- CWE-22 Improper Limitation of a Pathname to a Restricted Directory
References
Sources
- GitHub Advisory GHSA-xmj3-9gf7-h52w
Fencer Vulns keeps one entry per vulnerability: the CVE id wins, then GHSA, and records that alias each other are merged, except that two CVEs always keep separate entries. The same data is available as JSON.