Repository navigation
Conversation
Entries in a wheel's namespace_packages.txt were split on '.' and passed straight to os.path.join, which interprets its arguments as paths. An entry that is absolute, drive-qualified, or UNC therefore discarded the destination egg directory entirely and placed a fixed-content __init__.py at a path chosen by the wheel. The report framed this as Windows-only, but only the drive-letter form is; an absolute POSIX path escapes just as readily. Namespace entries are now validated as dotted module names on every platform, with a containment check as defense in depth. This sink is reachable only through the deprecated setup_requires mechanism when setup() is invoked outside a PEP 517 frontend. The setuptools build backend disables setup_requires installation in every hook, so it does not reach it. Ref GHSA-xmj3-9gf7-h52w. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
|
Tick the box to add this pull request to the merge queue (same as
|
|
Thanks for the fix. I reviewed the patch against the original issue and some additional edge cases, and I don't currently see a bypass.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hardening for GHSA-xmj3-9gf7-h52w, reported privately by @Hades2508.
The issue
Wheel._fix_namespace_packagessplit each entry of a wheel'snamespace_packages.txton.and passed the components straight toos.path.join, which interprets its arguments as paths:An entry with no dots and an absolute form is a single component, so
os.path.joindiscardsdestination_eggdirand resolves to theattacker-chosen path, where a fixed-content
__init__.pyis then created.The report framed this as Windows-only (drive-qualified paths), but only
that form is Windows-specific — an absolute POSIX path escapes just as
readily, confirmed end-to-end against the real
install_as_eggon macOS:Only bare
..was already neutral, since'..'.split('.')yields emptycomponents.
Impact
Low, and deliberately being handled in public rather than embargoed.
setuptools.installeris the only remaining caller ofinstall_as_egg(
easy_installis now a stub), so the sink is reachable only via thedeprecated
setup_requiresmechanism withsetup()invoked outside aPEP 517 frontend. The build backend does not reach it — every hook either
runs under
no_install_setup_requires()or underDistribution.patch(),whose
fetch_build_eggsreports requirements to the frontend instead ofinstalling them. Verified by instrumenting the sink:
Moreover, an attacker who can satisfy the precondition — controlling the
wheel resolved for a
setup_requiresentry — already gets arbitrary codeexecution, since
_fetch_build_eggsprepends the resulting egg tosys.pathfor the build to import. A fixed-content, non-overwriting__init__.pywhose parent directory must already exist is strictly weakerthan what that attacker already has.
The one residue not fully subsumed: writing
__init__.pyinto an existingPEP 420 namespace portion could alter import resolution. That motivates
fixing it as defense in depth.
The change
Namespace entries are dotted module names, so every component must be an
identifier. That single check rejects separators, absolute paths, drive
letters, UNC prefixes,
.., and empty components on every platform, and arealpathcontainment check follows as belt and braces — mirroring_resolve_destfrom #5325.Invalid entries now raise
ValueError, consistent with the othermalformed-wheel errors in this module.