Skip to content
Fencer Vulns
low —

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

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

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.