---
id: "GHSA-xmj3-9gf7-h52w"
kind: "vulnerability"
aliases: ["GHSA-xmj3-9gf7-h52w"]
title: "setuptools: Windows path escape via namespace_packages.txt during legacy wheel installation  Description"
severity: "low"
cvss: null
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "setuptools", "fixed": ["84.1.0"], "affected": ">= 38.2.2, < 84.1.0", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["cwe-22", "package", "pip", "python", "setuptools", "windows"]
not_affected_if: ["no-legacy-wheel-install", "non-windows", "setuptools-84.1.0-or-later"]
published_at: "2026-10-23T21:44:11+00:00"
first_seen_at: "2026-10-23T21:45:07+00:00"
updated_at: "2026-10-23T21:45:27+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-xmj3-9gf7-h52w", "first_seen_at": "2026-10-23T21:45:07+00:00"}]
---

# setuptools: Windows path escape via namespace_packages.txt during legacy wheel installation  Description

On Windows, a crafted wheel's namespace_packages.txt can make setuptools' legacy wheel-to-egg install write a fixed-content __init__.py to an attacker-chosen absolute path outside the install directory. The attacker controls only the location of the file, not its contents, and the write uses the installing process's permissions.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- pypi:setuptools affected >= 38.2.2, < 84.1.0 fixed 84.1.0

## How to check

pip show setuptools or python -m setuptools --version and compare against 84.1.0

## Fix

Upgrade setuptools to 84.1.0 or later.
Only install wheels from trusted indexes until upgraded.

## Timeline

- 2026-10-23T21:45:07+00:00 ghsa https://github.com/advisories/GHSA-xmj3-9gf7-h52w

## From the sources

### 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.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
