mirror of
https://git.yoctoproject.org/poky
synced 2026-09-19 00:49:33 +02:00
python3-setuptools: Fix CVE-2026-59890
This patch applies the upstream fix as referenced in [2],
using the commit shown in [1].
[1] dd9f436a36
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-59890
(From OE-Core rev: 0c89d54002ed0411ea34a926ccb80c4b6e4d858c)
Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
Signed-off-by: Yoann Congal <yoann.congal@smile.fr>
Signed-off-by: Paul Barker <paul@pbarker.dev>
This commit is contained in:
committed by
Paul Barker
parent
9b68c3fb71
commit
7a6a4aa3b1
@@ -0,0 +1,194 @@
|
||||
From d54dff79a9568c25551092711c7ebc28422006a4 Mon Sep 17 00:00:00 2001
|
||||
From: "Jason R. Coombs" <jaraco@jaraco.com>
|
||||
Date: Sat, 27 Jun 2026 10:46:34 -0400
|
||||
Subject: [PATCH] Normalize Unicode form when matching MANIFEST.in patterns
|
||||
|
||||
FileList matched MANIFEST.in patterns against on-disk names byte-for-byte
|
||||
with no Unicode normalization. On macOS APFS/HFS+, a file stored NFD and a
|
||||
pattern authored NFC denote the same file but differ byte-for-byte, so an
|
||||
exclude/global-exclude/recursive-exclude/prune rule could silently fail to
|
||||
drop a non-ASCII-named file, publishing it in the sdist despite the rule.
|
||||
|
||||
Normalize both the pattern and the candidate path to NFC before matching,
|
||||
via a new unicode_utils.normalize() helper and a _NormalizedMatcher wrapper
|
||||
around the compiled pattern in translate_pattern.
|
||||
|
||||
Fixes GHSA-h35f-9h28-mq5c.
|
||||
|
||||
CVE: CVE-2026-59890
|
||||
Upstream-Status: Backport [https://github.com/pypa/setuptools/commit/dd9f436a36486b4cb8a4c70a2321548b0be09b8f]
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||||
(cherry picked from commit dd9f436a36486b4cb8a4c70a2321548b0be09b8f)
|
||||
Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
|
||||
---
|
||||
newsfragments/+ghsa-h35f-9h28-mq5c.bugfix.rst | 7 +++
|
||||
setuptools/command/egg_info.py | 28 +++++++++++-
|
||||
setuptools/tests/test_manifest.py | 45 +++++++++++++++++++
|
||||
setuptools/unicode_utils.py | 14 ++++++
|
||||
4 files changed, 93 insertions(+), 1 deletion(-)
|
||||
create mode 100644 newsfragments/+ghsa-h35f-9h28-mq5c.bugfix.rst
|
||||
|
||||
diff --git a/newsfragments/+ghsa-h35f-9h28-mq5c.bugfix.rst b/newsfragments/+ghsa-h35f-9h28-mq5c.bugfix.rst
|
||||
new file mode 100644
|
||||
index 000000000..42d6c4cfe
|
||||
--- /dev/null
|
||||
+++ b/newsfragments/+ghsa-h35f-9h28-mq5c.bugfix.rst
|
||||
@@ -0,0 +1,7 @@
|
||||
+``MANIFEST.in`` matching (via ``FileList``) is now insensitive to Unicode
|
||||
+normalization form. A pattern authored in one form (e.g. NFC, as typically
|
||||
+saved by editors) now matches a file whose name is stored on disk in another
|
||||
+(e.g. NFD, as produced by macOS APFS/HFS+). Previously an ``exclude``,
|
||||
+``global-exclude``, ``recursive-exclude``, or ``prune`` rule could silently
|
||||
+fail to drop a non-ASCII-named file from the source distribution, publishing
|
||||
+it despite the exclusion -- see GHSA-h35f-9h28-mq5c.
|
||||
diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py
|
||||
index 62d2feea9..e858708ee 100644
|
||||
--- a/setuptools/command/egg_info.py
|
||||
+++ b/setuptools/command/egg_info.py
|
||||
@@ -34,6 +34,27 @@ from ..warnings import SetuptoolsDeprecationWarning
|
||||
PY_MAJOR = '{}.{}'.format(*sys.version_info)
|
||||
|
||||
|
||||
+class _NormalizedMatcher:
|
||||
+ """
|
||||
+ Wrap a compiled pattern so that matching is insensitive to Unicode
|
||||
+ normalization form.
|
||||
+
|
||||
+ File names walked from disk (NFD on macOS APFS/HFS+) and patterns from
|
||||
+ ``MANIFEST.in`` (typically NFC) can denote the same file while differing
|
||||
+ byte-for-byte. Normalizing both sides before matching keeps an exclusion
|
||||
+ (or inclusion) from silently failing. See GHSA-h35f-9h28-mq5c.
|
||||
+ """
|
||||
+
|
||||
+ def __init__(self, pattern: re.Pattern) -> None:
|
||||
+ self._pattern = pattern
|
||||
+
|
||||
+ def match(self, path):
|
||||
+ return self._pattern.match(unicode_utils.normalize(path))
|
||||
+
|
||||
+ def search(self, path):
|
||||
+ return self._pattern.search(unicode_utils.normalize(path))
|
||||
+
|
||||
+
|
||||
def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME
|
||||
"""
|
||||
Translate a file path glob like '*.txt' in to a regular expression.
|
||||
@@ -43,6 +64,11 @@ def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME
|
||||
"""
|
||||
pat = ''
|
||||
|
||||
+ # Normalize the pattern so it matches paths regardless of the Unicode
|
||||
+ # normalization form used on disk (GHSA-h35f-9h28-mq5c). Candidate paths
|
||||
+ # are normalized to the same form by ``_NormalizedMatcher``.
|
||||
+ glob = unicode_utils.normalize(glob)
|
||||
+
|
||||
# This will split on '/' within [character classes]. This is deliberate.
|
||||
chunks = glob.split(os.path.sep)
|
||||
|
||||
@@ -114,7 +140,7 @@ def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME
|
||||
pat += sep
|
||||
|
||||
pat += r'\Z'
|
||||
- return re.compile(pat, flags=re.MULTILINE | re.DOTALL)
|
||||
+ return _NormalizedMatcher(re.compile(pat, flags=re.MULTILINE | re.DOTALL))
|
||||
|
||||
|
||||
class InfoCommon:
|
||||
diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py
|
||||
index fbd21b197..1a7441fc3 100644
|
||||
--- a/setuptools/tests/test_manifest.py
|
||||
+++ b/setuptools/tests/test_manifest.py
|
||||
@@ -10,6 +10,7 @@ import io
|
||||
import logging
|
||||
from distutils import log
|
||||
from distutils.errors import DistutilsTemplateError
|
||||
+import unicodedata
|
||||
|
||||
from setuptools.command.egg_info import FileList, egg_info, translate_pattern
|
||||
from setuptools.dist import Distribution
|
||||
@@ -158,6 +159,21 @@ def test_translated_pattern_mismatch(pattern_mismatch):
|
||||
assert not translate_pattern(pattern).match(target)
|
||||
|
||||
|
||||
+def test_translate_pattern_unicode_normalization():
|
||||
+ """
|
||||
+ Matching is insensitive to Unicode normalization form: a pattern authored
|
||||
+ in one form matches a path stored on disk in another (and vice versa), so
|
||||
+ that an exclusion cannot be bypassed by an NFC/NFD mismatch.
|
||||
+
|
||||
+ Regression test for GHSA-h35f-9h28-mq5c.
|
||||
+ """
|
||||
+ nfc = unicodedata.normalize('NFC', 'café.txt') # 'café.txt' composed
|
||||
+ nfd = unicodedata.normalize('NFD', 'café.txt') # 'café.txt' decomposed
|
||||
+ assert nfc != nfd # the two byte forms genuinely differ
|
||||
+ assert translate_pattern(nfc).match(nfd)
|
||||
+ assert translate_pattern(nfd).match(nfc)
|
||||
+
|
||||
+
|
||||
class TempDirTestCase:
|
||||
def setup_method(self, method):
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
@@ -331,6 +347,35 @@ class TestManifestTest(TempDirTestCase):
|
||||
files = default_files | set([ml('app/a.txt'), ml('app/b.txt'), ml('app/c.rst')])
|
||||
assert files == self.get_files()
|
||||
|
||||
+ def test_global_exclude_unicode_normalization(self):
|
||||
+ """
|
||||
+ A ``global-exclude`` authored NFC must drop a file whose on-disk name
|
||||
+ is NFD: on macOS APFS/HFS+ the two are the same file, and even on
|
||||
+ case/normalization-exact filesystems the decomposed name can be
|
||||
+ committed and reach the build. Otherwise the file is published in the
|
||||
+ sdist despite the exclusion.
|
||||
+
|
||||
+ Regression test for GHSA-h35f-9h28-mq5c.
|
||||
+ """
|
||||
+ nfc_name = unicodedata.normalize('NFC', 'café.txt')
|
||||
+ nfd_name = unicodedata.normalize('NFD', 'café.txt')
|
||||
+ assert nfc_name != nfd_name
|
||||
+ # write the file under its decomposed (NFD) name ...
|
||||
+ touch(os.path.join(self.temp_dir, 'app', nfd_name))
|
||||
+ # ... and exclude it with the composed (NFC) form.
|
||||
+ self.make_manifest(
|
||||
+ f"""
|
||||
+ global-include *.txt
|
||||
+ global-exclude {nfc_name}
|
||||
+ """
|
||||
+ )
|
||||
+ leaked = {
|
||||
+ f
|
||||
+ for f in self.get_files()
|
||||
+ if unicodedata.normalize('NFC', os.path.basename(f)) == nfc_name
|
||||
+ }
|
||||
+ assert not leaked, f"excluded file leaked into manifest: {leaked}"
|
||||
+
|
||||
|
||||
class TestFileListTest(TempDirTestCase):
|
||||
"""
|
||||
diff --git a/setuptools/unicode_utils.py b/setuptools/unicode_utils.py
|
||||
index d43dcc11f..311d4075a 100644
|
||||
--- a/setuptools/unicode_utils.py
|
||||
+++ b/setuptools/unicode_utils.py
|
||||
@@ -15,6 +15,20 @@ def decompose(path):
|
||||
return path
|
||||
|
||||
|
||||
+def normalize(text):
|
||||
+ """
|
||||
+ Return *text* in a canonical Unicode form (NFC) so that names which are
|
||||
+ visually identical but encoded differently compare equal.
|
||||
+
|
||||
+ macOS APFS/HFS+ store file names in decomposed form (NFD), while patterns
|
||||
+ in ``MANIFEST.in`` are typically authored composed (NFC). The two denote
|
||||
+ the same file but differ byte-for-byte, so matching them directly lets an
|
||||
+ exclusion silently fail. Normalizing both the walked path and the pattern
|
||||
+ to a single form before matching avoids that (GHSA-h35f-9h28-mq5c).
|
||||
+ """
|
||||
+ return unicodedata.normalize('NFC', text) if isinstance(text, str) else text
|
||||
+
|
||||
+
|
||||
def filesys_decode(path):
|
||||
"""
|
||||
Ensure that the given path is decoded,
|
||||
--
|
||||
2.44.4
|
||||
@@ -15,6 +15,7 @@ SRC_URI += " \
|
||||
file://CVE-2024-6345.patch \
|
||||
file://CVE-2025-47273-pre1.patch \
|
||||
file://CVE-2025-47273.patch \
|
||||
file://CVE-2026-59890.patch \
|
||||
"
|
||||
|
||||
SRC_URI[sha256sum] = "5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"
|
||||
|
||||
Reference in New Issue
Block a user