Skip to content

Release 44.1.1.post3: fix CVE-2026-59890 (Python 2.7 line) - #6

Merged
icanhasmath merged 5 commits into
v44.1.1.xfrom
44.1.1.post3-security
Oct 22, 2026
Merged

icanhasmath merged 5 commits into
v44.1.1.xfrom
44.1.1.post3-security

Conversation

@icanhasmath

Copy link
Copy Markdown
Collaborator

ActiveState security release setuptools 44.1.1.post3 (Python 2.7 line).

CVE Level Fixed upstream in Status
CVE-2026-59890 (GHSA-h35f-9h28-mq5c) Moderate 83.0.0 Patched

The vulnerability

FileList matched MANIFEST.in patterns against on-disk names byte-for-byte. On macOS APFS/HFS+ a file stored NFD and a pattern authored NFC are the same file but different bytes. So an exclude / global-exclude / recursive-exclude / prune rule could silently miss a non-ASCII-named file, and the file ended up in the sdist.

The fix

Port of upstream dd9f436, which we already shipped as 82.0.1.post1 (PR #5). Both the pattern and the candidate path are normalized to NFC through a new unicode_utils.normalize() and a _NormalizedMatcher wrapper returned by translate_pattern().

Py2.7 adaptation: on Python 2, MANIFEST.in lines and walked file names are byte strings. Upstream's helper normalizes str only, so on Py2 it would have done nothing. normalize() therefore follows the existing decompose() helper: bytes are decoded as UTF-8, normalized and re-encoded, and left unchanged if they aren't UTF-8.

Validation (Python 2.7.18)

  • New tests: translate_pattern NFC↔NFD for text and bytes, plus an end-to-end egg_info global-exclude of an NFD-named file. All three fail on the pristine base and pass with the fix.
  • test_egg_info + test_sdist + test_manifest: 145 passed (pristine: 142, plus the 3 new tests).
  • On Py3.10 the egg_info-based tests fail with cannot import name 'sic' on the pristine base too (44.x predates 3.10's distutils shim). The translate_pattern test goes from fail to pass there as well.

Versioning moves to the .postN scheme: 44.1.1.post3 sorts after 44.1.1+security.2 and before 44.1.2. Tag v44.1.1.post3 will be created after merge.

icanhasmath and others added 2 commits October 22, 2026 10:47
…patterns

GHSA-h35f-9h28-mq5c. 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 returned by translate_pattern(). All five
FileList callers (exclude, recursive_exclude, prune, global_include,
global_exclude) only call .match() on the result, which the wrapper keeps.

Upstream: pypa/setuptools dd9f436 (v83.0.0); already shipped on our
82.0.1 line as v82.0.1.post1.

Py2.7 adaptation: on Python 2, MANIFEST.in lines and walked file names
are native byte strings, so normalize() follows the existing decompose()
helper: text is normalized directly; bytes are decoded as UTF-8,
normalized and re-encoded, and left unchanged if they are not UTF-8.
Upstream normalized str only, which on Python 2 would have made the fix
a no-op. No f-strings or type hints; _NormalizedMatcher is a new-style
class; test literals use \u escapes.

Tests: translate_pattern NFC<->NFD for text and (Py2) bytes, plus an
end-to-end egg_info global-exclude of an NFD-named file. All three fail
on the pristine base and pass with the fix under Python 2.7.18;
test_egg_info/test_sdist/test_manifest: 145 passed (pristine 142 + 3 new).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bump version 44.1.1+security.2 -> 44.1.1.post3 and add a changelog entry
for the backported MANIFEST.in Unicode-normalization fix.

New security releases use the PEP 440 .postN scheme in place of the
+security.N local label; N continues the line's count (44.1.1.1,
44.1.1+security.2, now 44.1.1.post3). 44.1.1.post3 sorts after
44.1.1+security.2 and before 44.1.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@ezequielp-activestate ezequielp-activestate left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 blockers. Approving, but please fix the tag_build problem below before tagging v44.1.1.post3.

The fix is a faithful port of upstream dd9f436a (shipped in v83.0.0). Upstream history from v83.0.0 to v84.2.1 has no follow-up that fixes or hardens it; the later commits touching egg_info.py, unicode_utils.py and test_manifest.py only change lint and typing. The Python 2 change, which decodes byte strings as UTF-8 before normalizing, is needed: upstream's isinstance(text, str) check would make the fix a no-op on Python 2.

🔴 Blockers: None

🟡 Should fix

  • Building from the source tree produces an invalid version. setup.cfg:1-3 still has [egg_info] tag_build = .post / tag_date = 1. With version = 44.1.1.post3, a pip wheel . or pip install . from the checkout or the tag gives Version: 44.1.1.post3.post-20261022, which packaging.Version rejects because it has two post segments. On the base branch the equivalent, 44.1.1+security.2.post20261022, is valid.
    • setuptools/tests/test_integration.py::test_stevedore passes on base and fails on head: stevedore raises InvalidVersion: '44.1.1.post3.post-2026…'.
    • Recent pip refuses wheels with invalid versions, so pip install git+…@v44.1.1.post3 would break the same way. Artifacts built through the release alias (egg_info -Db '') are fine.
    • Fix: clear tag_build and tag_date on this branch.
  • The version-ordering note leaves out v44.1.1.1. The 2023 GitHub release is named v44.1.1.1, and under PEP 440 44.1.1.1 sorts above 44.1.1.post3. That tag's own setup.cfg says version = 44.1.1, so this only matters if some index or catalog lists that build as 44.1.1.1. If one does, setuptools<45 would resolve to a build without CVE-2024-6345, CVE-2025-47273 or this fix. Worth confirming, or use a number above it such as 44.1.1.3.
  • On Python 2, an exclusion still leaks when the pattern and the path are different string types. This happens on base and head alike, for NFC against NFC too, so it isn't caused by this PR. With a byte-string MANIFEST.in pattern and a unicode entry in FileList.files (or the reverse), global_exclude leaves the file in. That can happen when a file-finder plugin returns text paths for non-ASCII names, and it causes the same harm the CVE describes. Cheap fix: in _NormalizedMatcher.match/.search (setuptools/command/egg_info.py:50-54), convert the path to the pattern's type (UTF-8 encode or decode) before matching.

⚪ Nits

  • setuptools/command/egg_info.py:35: only one blank line before class _NormalizedMatcher (flake8 E302).
  • The f5acb4f1 commit message says the test literals use \u escapes, but test_manifest.py:163-164 uses a literal é. It works because of the coding header; only the message is wrong.
  • include, recursive-include, graft and build_py.exclude_data_files still match without normalization, the same as upstream. The include cases fail safe; exclude_package_data is the same kind of bug but outside this advisory.

📋 Checked

  • Line by line against upstream: _NormalizedMatcher, the normalization in translate_pattern, and both new tests. The only differences are Python 2 adaptations.
  • Every translate_pattern caller: exclude, recursive_exclude, prune, global_include and global_exclude only test the match, which runs on a normalized copy, so FileList.files is never modified. 44.1.1 has no vendored copy of this code.
  • Reproduction: an NFC pattern against an NFD path. On base the file stays in the list for exclude, global_exclude, recursive_exclude and prune; on head all four remove it. A path that isn't valid UTF-8 doesn't crash and behaves as before.
  • Tests on Python 2.7.18: all 3 new tests fail on base and pass on head. test_manifest, test_egg_info and test_sdist give 145 passed on head. In the full setuptools + pkg_resources suites, base and head share the same 5 failures, all from the container; the only failure new on head is test_stevedore, from the first should-fix.
  • setup.cfg and .bumpversion.cfg agree, the CHANGES.rst entry cites the CVE and GHSA, and the changed code has no Python 3-only syntax.

⚠️ Not assessed: Python 3 runs, real macOS APFS/HFS+ (NFD names were simulated on Linux), and how the ActiveState Platform builds this package.

icanhasmath and others added 2 commits October 22, 2026 13:40
Review follow-up. On Python 2 a MANIFEST.in pattern and a FileList entry
can be of different string types, for example when a file-finder plugin
returns text paths for non-ASCII names. A byte pattern never matches a
non-ASCII text path or vice versa, so an exclusion leaked the file: the
same harm the CVE describes. This happened on the base too, NFC against
NFC included.

_NormalizedMatcher now brings the normalized path to the pattern's
string type (UTF-8 encode or decode; left unchanged if that fails)
before matching. New tests cover byte pattern against text path and
the reverse, NFC and NFD, through translate_pattern() and
FileList.global_exclude(); both fail without this change.

Also: add the second blank line before class _NormalizedMatcher (E302),
and spell the test literals as é escapes, as the f5acb4f message
already claimed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With version = 44.1.1.post3, the inherited [egg_info] tag_build = .post /
tag_date = 1 made a build from the source tree or tag (pip install .,
pip install git+...@v44.1.1.post3) report 44.1.1.post3.post-YYYYMMDD,
which PEP 440 rejects (two post segments). That broke
test_integration::test_stevedore and would make recent pip refuse the
wheel. A source build now reports 44.1.1.post3. The release alias
(egg_info -Db '') already cleared the tag, so it is unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@icanhasmath

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Pushed 96796ea and d522234.

🟡 tag_build / tag_date: removed the [egg_info] block from setup.cfg. setup.py egg_info from the tree now reports Version: 44.1.1.post3, so pip install . / git+…@v44.1.1.post3 and test_stevedore get a valid version. The release alias was already fine.

🟡 Version ordering vs 44.1.1.1: keeping 44.1.1.post3. This line follows the team's .postN scheme for new security releases. You're right that the platform inventory lists 44.1.1.1 and 44.1.1.2 as four-segment versions, and that 44.1.1.1 sorts above .post3 under PEP 440. That ordering risk is a known consequence of the scheme, and I'll note it in the release notes.

🟡 Mixed str/bytes on Python 2: fixed. _NormalizedMatcher now brings the normalized path to the pattern's string type (UTF-8 encode or decode, left unchanged on failure) before matching. A new test, test_translate_pattern_mixed_string_types, covers a byte pattern against a text path and the reverse, NFC and NFD, via translate_pattern() and FileList.global_exclude(). It fails on the previous matcher. Py2.7: test_egg_info + test_sdist + test_manifest give 147 passed.

⚪ E302: fixed.
⚪ Literal é: the test literals are now é escapes, which makes the f5acb4f message accurate. I left the pushed commit message itself alone rather than force-push.
⚪ include / graft / exclude_package_data: agreed these are unnormalized, as upstream is. The include side fails safe, and exclude_package_data is outside this advisory, so I left them out of this release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@ezequielp-activestate ezequielp-activestate left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of 8cfa412: should-fixes addressed. Approving.

Earlier finding Status Evidence (Python 2.7)
🟡 tag_build/tag_date produce an invalid version Fixed (d5222346) pip wheel . from the checkout now builds setuptools-44.1.1.post3-py2.py3-none-any.whl, a valid PEP 440 version. test_stevedore passes.
🟡 str/bytes pattern–path mismatch on Python 2 Fixed (96796eac) _NormalizedMatcher._prepare converts the path to the pattern's type, UTF-8 with a fallback on undecodable input. Both test_translate_pattern_mixed_string_types cases fail against the previous egg_info.py and pass here.
🟡 44.1.1.1 ordering Documented (8cfa4127) See below.
⚪ E302 before _NormalizedMatcher Fixed
⚪ literal é in the tests Fixed Now é.

Version ordering, to act on before release. The new CHANGES note says a build labelled 44.1.1.1 is listed in the ActiveState Platform inventory, and 44.1.1.post3 sorts below it under PEP 440. Keeping the team's .postN scheme is a reasonable call, but the note alone won't stop a resolver: setuptools<45 would still pick 44.1.1.1 if it's served next to this release. Someone needs to retire or relabel that inventory entry, or confirm the Platform's version ordering puts it below .post3, before this ships. I couldn't check the inventory myself.

📋 Checked: test_manifest.py and test_integration.py (manifest, normalization, mixed-type and stevedore tests): 73 passed.

⚪ Nit: three blank lines before test_translate_pattern_mixed_string_types in test_manifest.py (E303).

@icanhasmath
icanhasmath merged commit e2a1db3 into v44.1.1.x Oct 22, 2026
1 check failed
@icanhasmath
icanhasmath deleted the 44.1.1.post3-security branch October 22, 2026 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants