GHSA-xmj3-9gf7-h52w json
setuptools: Windows path escape via namespace_packages.txt during legacy wheel installation Description
published 25d ago modified 21d 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
Is GHSA-xmj3-9gf7-h52w in your dependencies? Fencer shows which of your repositories are affected as soon as an advisory lands, and opens the fix as a pull request.
Check with FencerAffected 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
- Maintainer advisory GHSA-xmj3-9gf7-h52w
Fencer Intel 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.
The maintainers of pypa/setuptools published their own advisory on GitHub. Reviewed sources take precedence over it.