setuptools: Windows path escape via namespace_packages.txt during legacy wheel installation DescriptionGHSA-xmj3-9gf7-h52w
Fix: pypa/setuptools#5329GHSA-xmj3-9gf7-h52w is a security vulnerability in setuptools. A fix is available for setuptools — see the affected versions and patch details below.
Real-World Exposure
setuptoolsReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
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:
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:
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:
__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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | setuptools | ≥ 38.2.2&&< 84.1.0 | 84.1.0pip install --upgrade 'setuptools==84.1.0' |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for setuptools, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update setuptools to 84.1.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-xmj3-9gf7-h52w is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
Frequently Asked Questions
Is GHSA-xmj3-9gf7-h52w in your dependencies?
Find it across PyPI, including transitive dependencies.