---
id: GHSA-xmj3-9gf7-h52w
title: >-
  setuptools: Windows path escape via namespace_packages.txt during legacy wheel
  installation  Description
summary: >-
  setuptools: Windows path escape via namespace_packages.txt during legacy wheel
  installation  Description
severity: low
cwe:
  - CWE-22
vendor: setuptools
product: setuptools
ecosystem: pip
affected:
  - 'setuptools >= 38.2.2, < 84.1.0'
patched:
  - setuptools 84.1.0
published: '2026-10-23'
updated: '2026-10-23'
sourceUpdated: '2026-10-23T21:44:13Z'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-xmj3-9gf7-h52w'
references:
  - url: 'https://github.com/pypa/setuptools/security/advisories/GHSA-xmj3-9gf7-h52w'
  - url: 'https://github.com/pypa/setuptools/pull/5329'
  - url: >-
      https://github.com/pypa/setuptools/commit/6d07b8d17890dda58d944ac51e58f54af92d52af
  - url: 'https://github.com/pypa/setuptools/releases/tag/v84.1.0'
  - url: 'https://github.com/advisories/GHSA-xmj3-9gf7-h52w'
tags:
  - ghsa
  - pip
ingestedAt: '2026-10-24T00:46:53.673Z'
---

## Overview

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

## Affected packages

- `setuptools >= 38.2.2, < 84.1.0`

## Remediation

Upgrade to a patched release:

- `setuptools 84.1.0`
