CVE-2026-59890: setuptools MANIFEST.in Exclusion Bypass Vulnerability

MODERATE
Advisory Identifier: GHSA-h35f-9h28-mq5c
CVSS 6.1
CWE-176 CWE-697
Published
Advisory Summary

CVE-2026-59890 is a MODERATE (CVSS 6.1) manifest.in exclusion bypass vulnerability (CWE-176: Improper Handling of Unicode Encoding) affecting setuptools libraries in the PyPI ecosystem. The issue involves manifest.in exclusion bypass in sdist via unicode normalization collision (nfc/nfd) on macos apfs/hfs+. Remediation is available by upgrading affected packages to version 83.0.0.

CVE-2026-59890 at a Glance

CVE Identifier CVE-2026-59890
GHSA Identifier GHSA-h35f-9h28-mq5c
Severity MODERATE
CVSS v3.1 Base Score 6.1
CWE Classification CWE-176: Improper Handling of Unicode Encoding
Affected Product setuptools
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 6.1

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N).

Attack Vector Local (AV:L)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity Low (I:L)
Availability None (A:N)

How to Fix CVE-2026-59890

To remediate this vulnerability, upgrade all affected setuptools dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

setuptools fixed: 83.0.0
pip install setuptools>=83.0.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show setuptools

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-59890, inspect your installed packages using pip:

pip show setuptools

Analyze the terminal output to locate any version of setuptools in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 83.0.0), your application is affected.

Note: The pip show setuptools command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-176: Improper Handling of Unicode Encoding, where the product does not properly handle when an input contains unicode encoding. In setuptools, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

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

CVE-2026-59890 Affected setuptools Versions

1 component(s)
Fixed Versions:
83.0.0
Product / Package Ecosystem Affected Versions Fixed Version
setuptools
PyPI
< 83.0.0
83.0.0

References

Frequently Asked Questions

What is CVE-2026-59890?
CVE-2026-59890 is a security vulnerability in setuptools involving MANIFEST.in exclusion bypass in sdist via Unicode normalization collision (NFC/NFD) on macOS APFS/HFS+ under CWE-176: Improper Handling of Unicode Encoding.
Which setuptools versions are affected?
Affected versions include releases prior to 83.0.0 across affected packages: setuptools.
What version fixes CVE-2026-59890?
Upgrading to 83.0.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is setuptools affected?
Yes, setuptools is affected in releases prior to 83.0.0. Upgrade your dependency to 83.0.0 or later.
What is the CVSS score of CVE-2026-59890?
CVE-2026-59890 has a CVSS v3.1 base score of 6.1 (MODERATE) with vector string CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N.
What CWE is associated with CVE-2026-59890?
CVE-2026-59890 is classified under CWE-176: Improper Handling of Unicode Encoding (The product does not properly handle when an input contains Unicode encoding.).
How can I check PyPI dependencies for CVE-2026-59890?
Run the dependency tree command: 'pip show setuptools'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-h35f-9h28-mq5c",
  "modified" : "2026-07-21T19:09:21Z",
  "published" : "2026-07-21T19:09:21Z",
  "aliases" : [ "CVE-2026-59890" ],
  "summary" : "setuptools: MANIFEST.in exclusion bypass in sdist via Unicode normalization collision (NFC/NFD) on macOS APFS/HFS+",
  "details" : "## Summary\n\nWhen 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.\n\n## Details\n\nFile 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:\n\n```python\n# setuptools/command/egg_info.py  (FileList.global_exclude)\ndef global_exclude(self, pattern):\n    match = translate_pattern(os.path.join('**', pattern))   # fnmatch.translate -> regex, no NFC/NFD\n    return self._remove_files(match.match)                   # byte-level regex over raw os.walk names\n```\n\nA 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.\n\nA `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.\n\n## Impact\n\n`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.\n\n## Proof of concept\n\nWith a project containing `MANIFEST.in`:\n\n```\nglobal-include *.txt *.json\nglobal-exclude secret_café.txt    # rule saved NFC\n```\n\nand 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.\n\n## Remediation\n\nNormalize 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.\n\n## Credit\n\nReported by Tomas Illuminati. Coordinated via CERT/CC VINCE VU#604762.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "setuptools"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "83.0.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/pypa/setuptools/security/advisories/GHSA-h35f-9h28-mq5c"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-59890"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/pypa/setuptools/commit/dd9f436a36486b4cb8a4c70a2321548b0be09b8f"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/pypa/advisory-database/tree/main/vulns/setuptools/PYSEC-2026-3447.yaml"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/pypa/setuptools"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/pypa/setuptools/releases/tag/v83.0.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-176", "CWE-697" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-21T19:09:21Z",
    "nvd_published_at" : "2026-07-08T17:17:27Z"
  }
}