Repository navigation
Commit b5cec4e
authored
fix(deps): Update dependency setuptools to v84 [SECURITY] (#418)
This PR contains the following updates:
| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [setuptools](https://redirect.github.com/pypa/setuptools) ([changelog](https://setuptools.pypa.io/en/stable/history.html)) | `82.0.1` → `84.1.0` |  |  |
---
### setuptools: MANIFEST.in exclusion bypass in sdist via Unicode normalization collision (NFC/NFD) on macOS APFS/HFS+
[CVE-2026-59890](https://nvd.nist.gov/vuln/detail/CVE-2026-59890) / [GHSA-h35f-9h28-mq5c](https://redirect.github.com/advisories/GHSA-h35f-9h28-mq5c)
<details>
<summary>More information</summary>
#### Details
##### Summary
When building a source distribution (`python -m build --sdist` / `setup.py sdist`), setuptools' `FileList` applies `MANIFEST.in` directives (`exclude`, `global-exclude`, `recursive-exclude`, `prune`) by matching a compiled glob against on-disk file names **byte-for-byte, with no Unicode normalization**. On normalization-preserving filesystems (notably macOS APFS and HFS+), a file written in NFD and a `MANIFEST.in` rule written in NFC refer to the same file but are byte-distinct, so the exclusion silently fails to match. A file the maintainer intended to exclude is then packed into the `.tar.gz` and, if published, uploaded to the public, immutable PyPI index.
##### Details
File names in `FileList.files` come from `os.walk` (`setuptools/_distutils/filelist.py`, `_find_all_simple`), so on APFS a file written NFD is offered to the matcher in NFD, while the `MANIFEST.in` pattern carries the author's editor form (typically NFC). The matching path performs no canonicalization:
```python
##### setuptools/command/egg_info.py (FileList.global_exclude)
def global_exclude(self, pattern):
match = translate_pattern(os.path.join('**', pattern)) # fnmatch.translate -> regex, no NFC/NFD
return self._remove_files(match.match) # byte-level regex over raw os.walk names
```
A rule written NFC (`café` = `63 61 66 c3 a9`) does not match an on-disk name written NFD (`café` = `63 61 66 65 cc 81`), even though the filesystem treats the two as one file.
A `unicodedata.normalize('NFD', ...)` helper exists in `setuptools/unicode_utils.py` (`decompose()`), but it is **never called in the manifest matching path**, so neither the pattern nor the walked path is normalized before matching. The only normalization in this area, `EggInfoCommand._manifest_normalize`, uses `filesys_decode` (bytes→str decode only, no NFC/NFD) and runs when writing `SOURCES.txt`, after matching has already occurred.
##### Impact
`MANIFEST.in` exclusions are the documented mechanism maintainers use to keep secrets, local configs, and private fixtures out of the published sdist. A non-ASCII excluded file may be published to the public, immutable PyPI index despite the rule — an irreversible disclosure with no visual cue (NFC and NFD forms render identically). Exposure is filesystem-dependent and most relevant on macOS APFS/HFS+, where many maintainers build and publish. Pure-ASCII rules are unaffected.
##### Proof of concept
With a project containing `MANIFEST.in`:
```
global-include *.txt *.json
global-exclude secret_café.txt # rule saved NFC
```
and an on-disk file `secret_café.txt` written in NFD, `python -m build --sdist` packs the secret file into the resulting `.tar.gz`, while an ASCII control file excluded by the same directive is correctly dropped — isolating the bypass to the NFC-pattern vs. NFD-name mismatch. Reproduced on macOS APFS with setuptools 82.0.1.
##### Remediation
Normalize both the walked path and each `MANIFEST.in` pattern to a single canonical form before matching, in both `setuptools/command/egg_info.py` (`FileList`) and the vendored `setuptools/_distutils/filelist.py`. For an exclusion list, err toward excluding more, and document that `MANIFEST.in` matching is normalization-insensitive on macOS.
##### Credit
Reported by Tomas Illuminati. Coordinated via CERT/CC VINCE VU#604762.
#### Severity
- CVSS Score: 6.1 / 10 (Medium)
- Vector String: `CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N`
#### References
- [https://github.com/pypa/setuptools/security/advisories/GHSA-h35f-9h28-mq5c](https://redirect.github.com/pypa/setuptools/security/advisories/GHSA-h35f-9h28-mq5c)
- [https://nvd.nist.gov/vuln/detail/CVE-2026-59890](https://nvd.nist.gov/vuln/detail/CVE-2026-59890)
- [https://github.com/pypa/setuptools/commit/dd9f436a36486b4cb8a4c70a2321548b0be09b8f](https://redirect.github.com/pypa/setuptools/commit/dd9f436a36486b4cb8a4c70a2321548b0be09b8f)
- [https://github.com/pypa/advisory-database/tree/main/vulns/setuptools/PYSEC-2026-3447.yaml](https://redirect.github.com/pypa/advisory-database/tree/main/vulns/setuptools/PYSEC-2026-3447.yaml)
- [https://github.com/pypa/setuptools/releases/tag/v83.0.0](https://redirect.github.com/pypa/setuptools/releases/tag/v83.0.0)
- [https://github.com/advisories/GHSA-h35f-9h28-mq5c](https://redirect.github.com/advisories/GHSA-h35f-9h28-mq5c)
This data is provided by the [GitHub Advisory Database](https://redirect.github.com/advisories/GHSA-h35f-9h28-mq5c) ([CC-BY 4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>
---
### setuptools: Windows path escape via namespace_packages.txt during legacy wheel installation Description
[GHSA-xmj3-9gf7-h52w](https://redirect.github.com/advisories/GHSA-xmj3-9gf7-h52w)
<details>
<summary>More information</summary>
#### Details
##### 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.
#### Severity
- CVSS Score: 1.0 / 10 (Low)
- Vector String: `CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:A/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N`
#### References
- [https://github.com/pypa/setuptools/security/advisories/GHSA-xmj3-9gf7-h52w](https://redirect.github.com/pypa/setuptools/security/advisories/GHSA-xmj3-9gf7-h52w)
- [https://github.com/pypa/setuptools/pull/5329](https://redirect.github.com/pypa/setuptools/pull/5329)
- [https://github.com/pypa/setuptools/commit/6d07b8d17890dda58d944ac51e58f54af92d52af](https://redirect.github.com/pypa/setuptools/commit/6d07b8d17890dda58d944ac51e58f54af92d52af)
- [https://github.com/pypa/setuptools/releases/tag/v84.1.0](https://redirect.github.com/pypa/setuptools/releases/tag/v84.1.0)
- [https://github.com/advisories/GHSA-xmj3-9gf7-h52w](https://redirect.github.com/advisories/GHSA-xmj3-9gf7-h52w)
This data is provided by the [GitHub Advisory Database](https://redirect.github.com/advisories/GHSA-xmj3-9gf7-h52w) ([CC-BY 4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>
---
### Release Notes
<details>
<summary>pypa/setuptools (setuptools)</summary>
### [`v84.1.0`](https://redirect.github.com/pypa/setuptools/compare/v84.0.0...v84.1.0)
[Compare Source](https://redirect.github.com/pypa/setuptools/compare/v84.0.0...v84.1.0)
### [`v84.0.0`](https://redirect.github.com/pypa/setuptools/compare/v83.0.0...v84.0.0)
[Compare Source](https://redirect.github.com/pypa/setuptools/compare/v83.0.0...v84.0.0)
### [`v83.0.0`](https://redirect.github.com/pypa/setuptools/compare/v82.0.1...v83.0.0)
[Compare Source](https://redirect.github.com/pypa/setuptools/compare/v82.0.1...v83.0.0)
</details>
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- At any time (no schedule defined)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTUuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE5NS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJzZWN1cml0eSJdfQ==-->1 parent 0ba5b2c commit b5cec4e
0 file changed
0 commit comments