VAITP Dataset

← Back to the dataset

CVE-2026-59890

setuptools: Unicode normalization flaw bypasses MANIFEST.in exclusions.

  • CVSS 6.1
  • CWE-176
  • Input Validation and Sanitization
  • Local

setuptools is a package that allows users to download, build, install, upgrade, and uninstall Python packages. Prior to 83.0.0, FileList applied MANIFEST.in exclude, global-exclude, recursive-exclude, and prune directives by matching compiled glob patterns against on-disk file names without Unicode normalization, so on macOS APFS or HFS+ an NFD file name could bypass an NFC exclusion rule and be packed into a source distribution. This issue is fixed in version 83.0.0.

CVSS base score
6.1
Published
2026-07-08
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Build/Package/Merge
Code defect classification
Incorrect Algorithm
Category
Input Validation and Sanitization
Subcategory
Information Disclosure
Accessibility scope
Local
Impact
Information Disclosure
Affected component
setuptools
Fixed by upgrading
Yes

Solution

Upgrade setuptools to version 83.0.0 or later.

Vulnerable code sample

import fnmatch
import unicodedata

# This is a simplified representation of the setuptools FileList logic 
# before the Unicode normalization fix.

def find_files_vulnerable(all_files, exclude_patterns):
    """
    Filters a list of files based on exclusion patterns, without
    performing Unicode normalization, mimicking the vulnerable behavior.
    """
    included_files = []
    for filename in all_files:
        is_excluded = False
        for pattern in exclude_patterns:
            # The core of the vulnerability: fnmatch compares raw strings.
            # It does not account for Unicode canonical equivalence.
            if fnmatch.fnmatch(filename, pattern):
                is_excluded = True
                break
        if not is_excluded:
            included_files.append(filename)
    return included_files

# --- Demonstration ---

# 1. A developer wants to exclude a file with a special character.
# They write the name in NFC (Normalized Form Composed), which is standard.
# 'í' is a single character \u00ed.
developer_exclusion_rule = "secret_fíle.txt" 

# 2. An attacker creates a file with the same visual name, but in NFD
# (Normalized Form Decomposed). On macOS, this is a valid filename.
# 'í' is represented as 'i' followed by a combining accent character ´ (\u0301).
attacker_filename_nfd = "secret_fi\u0301le.txt"

# Verify the strings are different byte-for-byte
# assert developer_exclusion_rule != attacker_filename_nfd
# But they are canonically equivalent
# assert unicodedata.normalize('NFC', attacker_filename_nfd) == developer_exclusion_rule

# 3. Simulate the list of files found on disk.
disk_files = ["setup.py", "my_module.py", attacker_filename_nfd]

# 4. Simulate the exclusion rules from a MANIFEST.in file.
manifest_exclusions = [developer_exclusion_rule]

# 5. Run the vulnerable logic.
packaged_files = find_files_vulnerable(disk_files, manifest_exclusions)

# The output would show that the attacker's file bypassed the exclusion.
# print(f"Files that would be packaged: {packaged_files}")
# Expected vulnerable result: ['setup.py', 'my_module.py', 'secret_fi\u0301le.txt']

Patched code sample

import unicodedata
import fnmatch

# A filename in NFD form, e.g., from a macOS filesystem ('n' + '˜')
filename_decomposed_nfd = 'man\u0303ana.txt'

# An exclusion pattern in NFC form, e.g., from a MANIFEST.in file ('ñ')
pattern_composed_nfc = 'ma\u00f1ana.txt'

# --- Fix Implementation ---
# The vulnerable behavior would be a direct comparison:
# fnmatch.fnmatch(filename_decomposed_nfd, pattern_composed_nfc) -> False

# The fix is to normalize the on-disk filename to NFC before the match.
normalized_filename = unicodedata.normalize('NFC', filename_decomposed_nfd)

# The corrected match now evaluates to True, correctly excluding the file.
is_excluded_after_fix = fnmatch.fnmatch(normalized_filename, pattern_composed_nfc)

# The result is now True, demonstrating the vulnerability has been fixed.
assert is_excluded_after_fix is True

Payload

import os
from pathlib import Path
import unicodedata

# Define the project structure
project_dir = Path("vulnerable_project")
package_dir = project_dir / "my_package"
package_dir.mkdir(parents=True, exist_ok=True)

# Create a minimal setup.py
(project_dir / "setup.py").write_text(
    """from setuptools import setup
setup(
    name='vulnerable-sdist-package',
    version='1.0.0',
    packages=['my_package'],
    include_package_data=True,
)
"""
)

# Create MANIFEST.in with an exclusion rule for the NFC-normalized filename.
# NFC is the standard form for 'crédentials'.
nfc_filename = unicodedata.normalize('NFC', 'secret-crédentials.txt')
(project_dir / "MANIFEST.in").write_text(
f"""graft my_package
global-exclude {nfc_filename}
"""
)

# Create an __init__.py to make 'my_package' a package
(package_dir / "__init__.py").touch()

# Create the sensitive file intended for exclusion, but with an NFD-normalized name.
# On macOS (APFS/HFS+), this NFD filename will bypass the NFC exclusion rule.
# NFD represents 'é' as 'e' + combining accent character.
nfd_filename = unicodedata.normalize('NFD', 'secret-crédentials.txt')
(package_dir / nfd_filename).write_text(
    "This is a secret file that should have been excluded."
)

Cite this entry

@misc{vaitp:cve202659890,
  title        = {{setuptools: Unicode normalization flaw bypasses MANIFEST.in exclusions.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-59890},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-59890/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::