Repository navigation
pip 24.0.post2 (Py3.7): backport CVE-2026-3219, 6357, 8643, 13346; vendored certifi 2026.10.19 - #5
Conversation
…ack_file GHSA-58qw-9mgm-455v. unpack_file ran zipfile.is_zipfile() for any archive whose name or content type pip did not already recognise. is_zipfile scans for a ZIP end-of-central-directory record anywhere in the file, so a tar+ZIP polyglot (a ZIP appended to a valid .tar.gz) was always handled as a ZIP, whatever its file name, letting an attacker pick which set of files pip actually extracts. Choose the format by decreasing reliability: content type, then file name extension, then magic signature, and only trust the magic signature when it is unambiguous. A file that is both a valid zip and a valid tar with no other hint is now rejected with InstallationError. Upstream: pypa#13870 (c4cdbf9), released in 26.1. Applied verbatim apart from typing.Optional in place of 'str | None'. Tests mirror upstream; mock call_args.kwargs (3.8+) is replaced by call_args[1] for Python 3.7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GHSA-jp4c-xjxw-mgf9. pip ran its self-version check in a `finally` after the command body, so after `pip install` had already written wheels into site-packages. The check builds a PackageFinder, and the vendored packaging.tags then probes `import _manylinux` (the PEP 600 override hook). A just-installed wheel that ships a top-level `_manylinux.py` was therefore imported and executed by pip itself. Reproduced on pristine 24.0 under Python 3.7 with a wheel containing only `_manylinux.py`: the module ran from the post-install check. Split the check into a fetch phase, which runs before the command body (remote lookup, state file, "installed by pip" decision), and an emit phase, which only logs the prompt afterwards: - Command.handle_pip_version_check(options) becomes the context manager Command.pip_version_check(options, args) and wraps the command body in Command._main. - IndexGroupCommand fetches before yielding and emits in `finally`, each guarded by the old "error checking the latest version" handling. - InstallCommand skips the check when pip itself is a requirement. - self_outdated_check gains pip_self_version_check_fetch / _emit and _compute_upgrade_prompt; the installer is read from the dist found during fetch instead of re-querying the environment afterwards (was_installed_by_pip and _self_version_check_logic are removed, as upstream). Upstream: pypa#13923 (b369bfc), released in 26.1. Adaptation: 24.0 has no index_command.py or _run_wrapper; the context manager lives in cli/req_command.py and wraps run() in _main. typing Iterator/List/Optional for Python 3.7. pip_self_version_check(session, options) is kept as a fetch+emit wrapper so the old entry point still works. 24.0's check has no externally-managed short-circuit, so that part of upstream does not apply. Tests follow upstream, plus one asserting the fetch -> body -> emit order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pts dir GHSA-wf93-45jw-7689. pip joined console_scripts / gui_scripts entry point names onto the scripts directory without checking that the result stays inside it. The vendored distlib 0.3.8 accepts any non-whitespace name (ENTRY_RE '[^\[]\S*') and, unlike distlib 0.4.2, does no containment check of its own, so a wheel with an entry point such as '../../../../tmp/escaped-script = pkg:main' wrote a script outside the environment. Reproduced on pristine 24.0 under Python 3.7. Add _script_within_dir() and have _raise_for_invalid_entrypoint() (called from PipScriptMaker.make with self.target_dir) raise InstallationError for any name that resolves outside the scripts dir. Upstream: pypa#14000 (8eb1784), released in 26.1.2. Applied verbatim; tests mirror upstream. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GHSA-qwm4-qh6w-59xr. Link.path already percent-decodes the URL path, but Link.filename unquoted the basename a second time, so a doubly-encoded separator from a malicious index (..%252F..%252Fevil.whl) became a real "../../evil.whl" and the download was written outside the download directory. This matters most for `pip download --only-binary`. On pristine 24.0 such a link yields filename '../../../evil-1.0-py3-none-any.whl'. - Link.filename no longer decodes twice and always returns a PathComponent, a single path component (no separator, never "." or ".."), falling back to the netloc. - New helpers in pip._internal.models.link: PathComponent (NewType), _to_path_component, as_path_component and join_within_directory. The HTTP response file name, including the Content-Disposition value (where a bare ".." previously got through sanitize_content_filename), is validated with as_path_component. - All four download-path joins go through join_within_directory: Downloader, BatchDownloader, _check_download_dir and RequirementPreparer.save_linked_requirement. Upstream: pypa#14110 (10dfb6b), released in 26.2.0. Adaptation: typing.NewType is available on Python 3.7, so upstream's PathComponent type is kept. Because NewType is not enforced at runtime, join_within_directory also re-validates its argument and raises ValueError, matching the ActiveState 20.3.4.post2 backport (stricter than upstream). 24.0 has BatchDownloader in addition to Downloader, and both are covered. Tests mirror upstream's test_link.py additions, plus download-level tests for the doubly-encoded URL and Content-Disposition "..". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The vendored certifi 2023.7.22 still trusts root certificates that have since been removed for cause: - GHSA-6q48-6c7w-9gw2 (critical, no CVE yet): emSign ECC Root CA - G3, whose intermediate CA keys were compromised (removed upstream in certifi 2026.10.19) - CVE-2024-39689 (GHSA-248v-346w-9cwc): GLOBALTRUST 2020 roots (affected since certifi 2021.5.30, removed upstream in certifi 2024.7.4) Replace it with certifi 2026.10.19, the release vendored by upstream pip 26.2.2. LICENSE, __init__.py, __main__.py, cacert.pem, core.py and py.typed are byte-identical to pip 26.2.2's vendored copy (certifi's own tests/ directory is not vendored, as before). core.py's Python 3.7 branch (importlib.resources.path / read_text) is unchanged from upstream, so no port is needed on this line. tools/vendoring/patches/certifi.patch is pip 26.2.2's and reproduces the vendored core.py from upstream certifi 2026.10.19. CVE-2022-23491 (TrustCor) and CVE-2023-37920 (E-Tugra) were already fixed by the 2023.7.22 bundle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3346, CVE-2026-3219; vendored certifi 2026.10.19 Bump version 24.0 -> 24.0.post2 and add a NEWS section covering the four pip CVE backports and the vendored certifi upgrade (GHSA-6q48-6c7w-9gw2, CVE-2024-39689). The NEWS note records CVE-2025-8869 and CVE-2026-1703 as patched in the earlier 24.0.0.1 build, which kept the internal version 24.0. .post2 because 24.0.0.1 was ActiveState release 1 on this line; that keeps the override-table version (24.0.0.2) distinct from 24.0.0.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ezequielp-activestate
left a comment
There was a problem hiding this comment.
0 blockers. Approving.
The four CVE backports match upstream, and the vendored certifi is upstream 2026.10.19.
🔴 Blockers: None
🟡 Should fix
src/pip/_internal/operations/install/wheel.py:420:_script_within_dirusesdest.startswith(root + os.sep), so_script_within_dir("pip", "/")isFalse. With a scripts dir of/orC:\, every entry point fails to install. Upstream has the same line, and upstream's lateris_within_directory-based version also rejects/. A prefix ofroot if root.endswith(os.sep) else root + os.sepfixes it.
⚪ Nits
- A
..download name (Content-Disposition: filename="..") surfaces as a rawValueErrortraceback (network/download.py:120→models/link.py:67). The download is still blocked. src/pip/_internal/cli/base_command.py:239: the version-check block sits outside pip's error interception, so a Ctrl-C during the pre-command fetch (up to 5s) shows a traceback instead of "Operation cancelled by user". Upstream puts it inside_inner_run.
📋 Checked
- CVE-2026-3219, 6357, 8643 and 13346 match pypa#13870, pypa#13923, pypa#14000 and pypa#14110. All four download write sites use
join_within_directory. - certifi:
vendor.txt,__version__and the title agree on2026.10.19. The files are byte-identical to the PyPI wheel apart from thepip._vendor.certifirename.core.pykeeps theimportlib.resourcesbranch Python 3.7 needs. - Tests on Python 3.7: full
tests/unitgives 1592 passed; the 4 failures are because the container has no git. 71 of the new tests fail on the base source.
- Functional tests and Windows.
- The end-to-end
_manylinuxreproduction.
…t_within_dir Review findings on #5 (and the same findings on #4/#6/#7): - is_within_directory used os.path.commonpath (from the 24.0.0.1 CVE-2026-1703 backport), which drops a POSIX "//" prefix, so `pip install --prefix //tmp/x <wheel>` failed with "trying to install outside the target directory". On Windows commonpath also raises ValueError for a target on another drive. - _script_within_dir used dest.startswith(root + os.sep), so with a scripts directory at a filesystem root ("/" or "C:\") every entry point was rejected. is_within_directory now compares on a path-component boundary without commonpath: target == directory, or target starts with the directory plus a separator (not doubled when the directory is already a root). _script_within_dir reuses it and still rejects a name that resolves to the scripts directory itself ("." / ""). Upstream fixed the "//" case in pypa/pip 7ff8bdd with the startswith(abs_dir + os.sep) form, which rejects everything under "/"; this form handles both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) Review finding (raised as a blocker on #4/#6/#7; confirmed on this line too): the CVE-2025-8869 backport shipped in 24.0.0.1 only checks that a symlink's target is another archive member, and "." / ".." can be members. Two archive shapes therefore still wrote outside the extract directory: - symlink `outside_link -> ..`, then `outside_link/outside.txt`, then a `..` directory member (the error came only after the write); - `.`, `redir -> .`, `redir/up -> ..`, then `redir/up/outside.txt` (no error at all). pip 24.0 has no tarfile.data_filter path, so on this line untar_file's member loop is the only guard, for every Python version. Port upstream pypa#14127 (17eb176, released in 26.2): - is_within_directory gains a keyword-only `resolve_symlinks` option that compares realpath()s; - each member is rejected if its path escapes either textually or after resolving symlinks; - a symlink is rejected if its target resolves outside the destination, before anything can be written through it. Upstream's two regression tests are included and fail without the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review finding on #4 (same code on this line): the CVE-2025-8869 symlink check looked up the link target with tar.getmember(), which needs the exact member name. Two valid archive shapes were therefore refused on the tar fallback path: - "./"-prefixed archives (as produced by `tar -C dir .`), e.g. `./pkg/link -> target.txt`; - a symlink to a directory that has no directory entry of its own. is_symlink_target_in_tar now compares against the set of normalized member paths plus every implied parent directory, computed once per archive (keyword-only `member_paths`) to avoid O(n^2) work. Upstream 26.2.2 still refuses both shapes; this is a deliberate improvement. Containment is still enforced by the realpath checks from the previous commit (pypa#14127), and a link to a path that is not in the archive is still refused. Tests cover all three cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review finding on #5 (nit) and #3 (should fix): the CVE-2026-6357 port entered `with self.pip_version_check(...)` outside the intercepts_unhandled_exc wrapper, so anything raised while fetching (up to 5s, before the command body) escaped pip's error handling: a Ctrl-C showed a raw KeyboardInterrupt traceback instead of "Operation cancelled by user", and `pip install "foo @ http://[bad/x"` died with a bare ValueError (exit 1) from _arg_refers_to_pip. - base_command: wrap the `with pip_version_check(...)` block and self.run() in an inner function and intercept that, as upstream does with _inner_run (debug mode still calls it directly). - install: _arg_refers_to_pip also returns False on a plain ValueError from the vendored packaging/urllib.parse, so a malformed requirement is reported by pip's normal requirement handling. On this line that restores the 24.0.0.1 behaviour for that input (exit 2, pip's "ERROR: Exception:" report). Tests cover KeyboardInterrupt and ValueError raised from the fetch, and _arg_refers_to_pip with the malformed URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…back Review nit on #5 (and #3/#4/#6/#7): when a server's Content-Disposition (or a URL) yields a download name of ".." or ".", as_path_component raised a bare ValueError and pip printed a traceback. The write was already blocked; only the reporting was poor. Add pip._internal.exceptions.UnsafeFilenameError, a subclass of both InstallationError and ValueError, and raise it from as_path_component and join_within_directory. Existing callers and tests that expect ValueError keep working, and pip's command loop now reports it as "ERROR: Unexpected file name derived from URL: '..'" (exit 1) instead of "ERROR: Exception:" plus a traceback (exit 2). Verified end to end against a local server sending `Content-Disposition: filename=".."`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The 24.0.post2 note used `.. note` without `::`, which reStructuredText treats as a comment, so it did not render (review nit on #3/#7; same here). It is now a real `.. note::` admonition. The upstream header comment at the top of the file is left as is. - Record the review fixes in the 24.0.post2 section: the CVE-2025-8869 hardening (pypa#14127; the 24.0.0.1 fix was incomplete), and under Bug Fixes the "//" prefix regression (from the 24.0.0.1 CVE-2026-1703 fix), scripts directories at a filesystem root, "./"-prefixed and implicit-directory symlinks, error handling around the version-check fetch, and the clean error for "." / ".." download names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
posixpath.normpath keeps a leading "//" and posixpath.dirname("//") is
"//", so collecting the implied parent directories of a member named
"//x/y" never terminated: a crafted archive could hang pip in the tar
fallback path. Stop walking parents once dirname() stops changing the
name, and add a regression test.
The bug was in the shared review-round helper (item G) and was caught on
the 26.0.1 line; this applies the same guard here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer finding (#3 should-fix, #5 nit; same code on this line): the CVE-2026-6357 port wrapped the already-intercepted `run` in `with self.pip_version_check(...)`, so the pre-command fetch ran outside intercepts_unhandled_exc. A ValueError from `_arg_refers_to_pip` (urlparse's "Invalid IPv6 URL" for `pip install "foo @ http://[bad/x"`) or a Ctrl-C during the fetch escaped as a raw, uncaught traceback (exit 1). Move the `with` into an inner function that is itself wrapped by intercepts_unhandled_exc, which is where upstream has it (`_inner_run`). Also let `_arg_refers_to_pip` treat a bare ValueError like InvalidRequirement (not pip), so the malformed requirement reaches the normal requirement parser. `pip install "foo @ http://[bad/x"` now behaves exactly as on pristine 21.3.1 (pip's "ERROR: Exception:" report, exit 2); a Ctrl-C during the fetch prints "Operation cancelled by user". Tests cover both, plus _arg_refers_to_pip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t_within_dir Reviewer findings (#7, should fix; same on #4/#5/#6): - is_within_directory used os.path.commonpath, which drops the POSIX "//" prefix, so `pip install --prefix //tmp/x <wheel>` failed with "trying to install outside the target directory" (already broken on the 26.0.1 base). Upstream fixed it in 7ff8bdd (pypa#14001, 26.1.2) with startswith(abs_dir + os.sep), but that form rejects every target when the directory is a filesystem root ("/" or "C:\\"). - _script_within_dir used dest.startswith(root + os.sep), so a scripts directory of "/" or "C:\\" rejected every console/gui script. Both now use one prefix rule: a target is within a directory when it equals it or starts with the directory plus a separator, where a directory that already ends with a separator (a filesystem root) is used as-is. No commonpath, so a target on another Windows drive is simply outside instead of raising ValueError. _script_within_dir delegates to is_within_directory and still refuses a name that resolves to the scripts directory itself. Tests cover "//" roots, "/" as the directory and as the scripts directory, and the other-drive case on Windows; the existing CVE-2026-1703 prefix-sibling and CVE-2026-8643 cases still pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the approval. Cross-checking against the other PRs found two more problems on this line, both inherited from the 24.0.0.1 release, so I fixed those as well. The five PRs were reviewed together before fixing, because most findings share a root cause. Every line got the same helper code from one shared spec, and some findings were applied to lines where they were not flagged. Everything is in new commits on top of the reviewed head, with no force-push.
Your nits:
Also:
Re-verified on a wheel built from the new head, in the line's Docker image:
🤖 Generated with Claude Code |
ezequielp-activestate
left a comment
There was a problem hiding this comment.
Re-review of 6837c78: all earlier findings fixed. Approving.
| Earlier finding | Status | Evidence |
|---|---|---|
🟡 root scripts dir (wheel.py:420) |
Fixed | The new test fails on the old code. |
| ⚪ Ctrl-C during the version check | Fixed | The version check runs inside intercepts_unhandled_exc through _inner_run. The KeyboardInterrupt and ValueError tests fail on the old code. |
⚪ raw ValueError on a .. download name |
Fixed | Now UnsafeFilenameError. Three test_link cases fail on the old code. |
Also brought in: pypa#14127, the // fix, member-name normalization, and _arg_refers_to_pip catching ValueError.
- Tar escape (pypa#14127): with
tarfile.data_filterremoved, both shapes (outside_link -> ..+outside_link/outside.txt+..; and.,redir -> .,redir/up -> ..,redir/up/outside.txt) wroteoutside.txtoutside the extract dir on the old head. They now raiseInstallationErrorand nothing is written. Five more attack shapes (chained, absolute, leading-dir, nested, deep-to-shallow symlinks) are rejected. Valid archives that were wrongly rejected before (./-prefixed, a link to an implied directory, a destination path containing a symlink) now extract.
Tests (Python 3.7): full tests/unit gives 1613 passed on the new head, against 1592 on the old one. The same 4 failures occur on both, because the container has no git.
⚪ Nit (all five lines): _tar_member_paths never adds "." (the while name not in ("", ".", "/") loop in utils/unpacking.py), so is_symlink_target_in_tar rejects a symlink that resolves to the archive root even when the archive has an explicit . member. For example, a tar -C dir . archive containing ./sub/root -> .. used to extract and now fails with InstallationError. Upstream also rejects that link whenever . is not an explicit member. There is no security impact, since this direction only adds rejections.
…26.10.19 (#7) * Backport CVE-2026-3219: disambiguate tar/zip archive detection in unpack_file GHSA-58qw-9mgm-455v. unpack_file() treated any file that zipfile.is_zipfile() accepted as a zip, regardless of its name or whether it was also a valid tar. A tar+zip polyglot could therefore install different contents from what its .tar.gz name suggests. Choose the format from the content type, then the file extension, then the magic signature, and refuse to unpack a file whose signature matches both formats. Upstream: pypa#13870 (c4cdbf9), released in 26.1. Source change cherry-picked unchanged; the test import block was resolved against 26.0.1, which lacks the _get_default_mode_plus_executable import the upstream test file had picked up in the meantime (unused by these tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Backport CVE-2026-8643: reject entry point names that escape the scripts dir GHSA-wf93-45jw-7689. pip joined console_scripts / gui_scripts entry point names onto the scripts directory without checking that the result stays inside it. The vendored distlib 0.4.0 accepts any non-whitespace name (ENTRY_RE name group [^\[]\S*) and its ScriptMaker has no directory guard, so a wheel declaring e.g. '../../outside = pkg:main' or an absolute path wrote a script outside the scripts directory on 26.0.1. Add _script_within_dir() and have _raise_for_invalid_entrypoint() (called from PipScriptMaker.make with self.target_dir) raise InstallationError for any name that resolves outside the scripts dir. Upstream: pypa#14000 (8eb1784), released in 26.1.2. Cherry-picked unchanged, including tests and news fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Backport CVE-2026-13346: stop Link.filename decoding the URL path twice GHSA-qwm4-qh6w-59xr. Link already percent-decodes the URL path into self._path, but Link.filename unquoted the basename again, so a doubly-encoded separator from a malicious index (..%252F..%252Fevil.whl) became a real '../../evil.whl' and the download was written outside the download directory. This matters most for `pip download --only-binary`. Drop the second decode, make Link.filename always return a single path component (PathComponent), validate the HTTP response file name with as_path_component(), and route the download-path joins in Downloader, _check_download_dir and RequirementPreparer.save_linked_requirement through join_within_directory(). Upstream: pypa#14110 (10dfb6b), released in 26.2.0. Cherry-picked; only the import blocks in download.py and prepare.py conflicted and were resolved against 26.0.1's imports (26.0.1 still imports PyPI in download.py and does not need DirectUrl in prepare.py). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Backport CVE-2026-6357: run the pip self-version check before installing GHSA-jp4c-xjxw-mgf9. The self-version check ran after the command body in a finally block, and its imports are deliberately deferred (cli/index_command.py lazily imports pip._internal.self_outdated_check and its dependencies). After `pip install` had just written new wheels into site-packages, those deferred imports could resolve to modules from the freshly installed packages, e.g. a top-level _manylinux.py, which packaging's tag logic imports while the check builds its PackageFinder. Split the check into a fetch phase that runs before the command body (all network and metadata work, and every import) and an emit phase that only logs the captured UpgradePrompt afterwards. Command.handle_pip_version_check() becomes the pip_version_check() context manager; InstallCommand skips the check when pip itself is a requirement. Upstream: pypa#13923 (b369bfc), released in 26.1. Cherry-picked; only the index_command.py import block conflicted (26.0.1 also imports sys there). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Upgrade vendored certifi to 2026.10.19 GHSA-6q48-6c7w-9gw2 (critical, no CVE): certifi releases from 2019.03.09 up to 2026.10.19 bundle the emSign ECC Root CA - G3, whose intermediate CA keys were compromised. The vendored certifi 2026.1.4 is affected. Vendor certifi 2026.10.19, the release vendored by upstream pip 26.2.2. LICENSE, __init__.py, __main__.py, cacert.pem, core.py and py.typed are byte-identical to pip 26.2.2's vendored copy (pip 26.2.2 also carries a certifi/tests directory that 26.0.1 never vendored; it is not added). core.py and tools/vendoring/patches/certifi.patch are unchanged from 26.0.1, so the patch still applies. The bundle drops emSign ECC Root CA - G3 and the other roots Mozilla removed since 2026.1.4 (136 -> 120 certificates). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Release 26.0.1.post1: backport four pip CVEs and vendored certifi 2026.10.19 Bump version 26.0.1 -> 26.0.1.post1 (PEP 440 post-release) and add a NEWS section covering CVE-2026-3219, CVE-2026-6357, CVE-2026-8643, CVE-2026-13346 and the vendored certifi upgrade (GHSA-6q48-6c7w-9gw2, emSign). CVE-2026-1703 and CVE-2025-8869 are noted as already fixed in 26.0.1. The upstream news/ fragments brought in by the cherry-picks are removed: their content is folded into the NEWS section above, and leaving them would make a towncrier run list the same changes again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: harden the CVE-2025-8869 fix (port pypa#14127) Reviewer finding (#7, blocker): 26.0.1 has upstream's 25.3 fix for CVE-2025-8869 (GHSA-4xh5-x5gv-qwph) but not the later hardening from pypa#14127 (17eb176, released in 26.2). On the tar fallback path used when tarfile.data_filter is missing (Python 3.9.0-3.9.16), symlinks to "." or ".." are archive members, so is_symlink_target_in_tar accepts them, and a later member written through them lands outside the extract directory: - outside_link -> .., outside_link/outside.txt, then a ".." dir member: the file is written before the InstallationError is raised; - ., redir -> ., redir/up -> .., redir/up/outside.txt: no error at all. Port 17eb176: is_within_directory gains a keyword-only resolve_symlinks flag (realpath instead of abspath), each member is also checked after resolving symlinks, and a symlink whose target resolves outside the destination is rejected before it is created. Upstream's two regression tests are included. The only conflict was the is_within_directory docstring; the commonpath body is replaced separately in the next review commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: root-safe path containment for is_within_directory and _script_within_dir Reviewer findings (#7, should fix; same on #4/#5/#6): - is_within_directory used os.path.commonpath, which drops the POSIX "//" prefix, so `pip install --prefix //tmp/x <wheel>` failed with "trying to install outside the target directory" (already broken on the 26.0.1 base). Upstream fixed it in 7ff8bdd (pypa#14001, 26.1.2) with startswith(abs_dir + os.sep), but that form rejects every target when the directory is a filesystem root ("/" or "C:\\"). - _script_within_dir used dest.startswith(root + os.sep), so a scripts directory of "/" or "C:\\" rejected every console/gui script. Both now use one prefix rule: a target is within a directory when it equals it or starts with the directory plus a separator, where a directory that already ends with a separator (a filesystem root) is used as-is. No commonpath, so a target on another Windows drive is simply outside instead of raising ValueError. _script_within_dir delegates to is_within_directory and still refuses a name that resolves to the scripts directory itself. Tests cover "//" roots, "/" as the directory and as the scripts directory, and the other-drive case on Windows; the existing CVE-2026-1703 prefix-sibling and CVE-2026-8643 cases still pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: accept normalized member names in is_symlink_target_in_tar Reviewer finding (#4, should fix; the same code is on every AS pip line): on the tar fallback path (no tarfile.data_filter), the symlink check looked the link target up with tar.getmember(), which needs the exact stored member name. Two valid archive shapes were therefore refused: - "./"-prefixed archives, as produced by `tar -C dir .`, e.g. ./pkg/link -> target.txt (stored as "./pkg/target.txt"); - a symlink to a directory that has no directory entry of its own and only exists as the parent of other members. Upstream 26.2.2 behaves the same; this is a deliberate improvement. The link target is now compared against the set of normalized member paths plus every implied parent directory. That set is computed once per archive and passed in (is_symlink_target_in_tar gains a keyword-only member_paths argument), so extraction stays linear. Containment is still enforced by the realpath checks from the previous review commit (pypa#14127): a link to "." or ".." or to an absolute path is rejected there, and a link to a path that is not in the archive is still refused here. The parent walk stops at any fixed point: posixpath.dirname("//") is "//", so a "//"-prefixed member name would otherwise loop forever. Tests: both valid shapes unpack, a link to a non-member is refused, and collecting member paths terminates for a "//" member name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: report an unsafe download file name as a pip error, not a traceback Reviewer nit (#7; the same on #3-#6): when a download's file name is not a single safe path component, e.g. `Content-Disposition: filename=".."` with a content type that adds no extension, as_path_component raised a bare ValueError and pip printed a traceback (exit 2). The write was already blocked; only the reporting was poor. Upstream behaves the same. Add UnsafeFilenameError(InstallationError, ValueError) to pip's exceptions and raise it instead. It is still a ValueError, so existing callers and tests keep working, and pip's command loop reports InstallationError as a plain line. With a local server returning filename="..": before: Traceback ... ValueError: Unexpected file name derived from URL: '..' after: ERROR: Unexpected file name derived from URL: '..' (exit 1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: let _arg_refers_to_pip tolerate a bare ValueError Reviewer finding (#3, should fix; applied to every AS pip line for consistency): the CVE-2026-6357 backport calls _arg_refers_to_pip() before the command body to decide whether `pip install pip` should skip the version check. It only caught InvalidRequirement, but packaging can surface a bare ValueError (urlsplit's "Invalid IPv6 URL" for `foo @ http://[bad/x`), which would crash the pre-command check instead of letting pip's normal requirement parsing report the error. On this line the version check already runs inside _inner_run (pip's error handling), and 26.0.1's vendored packaging parses that input without raising, so this is defensive: catch (InvalidRequirement, ValueError) and return False. Note that `pip install --no-index "foo @ http://[bad/x"` still ends in a ValueError traceback raised later from Link.__init__; that is unchanged upstream 26.0.1 behaviour and outside this backport. Tests: the ValueError case (fails before this change) plus basic name matching. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: NEWS — render notes, record review fixes Reviewer nits (#7): the 26.0.1.post1 section's `.. note` lacked `::`, so reStructuredText treated it as a comment and it never rendered; and it claimed CVE-2025-8869 was "already fixed" in 26.0.1 although 26.0.1 lacked the pypa#14127 hardening. - Make the note a real `.. note::` admonition (upstream's header comment on line 1 is intentionally left as a comment). Checked with docutils: the note now renders. - CVE-2025-8869: say it was fixed in 26.0.1 and is hardened here, and add a Security Fixes entry for the pypa#14127 port. - Add a Bug Fixes subsection for the other review fixes: root-safe is_within_directory / _script_within_dir, normalized symlink member names, the clean error for unsafe download file names, and the _arg_refers_to_pip ValueError tolerance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
_tar_member_paths never recorded ".", so is_symlink_target_in_tar refused a
symlink that resolves to the archive root even when the archive has an
explicit "." member. A "tar -C dir ." archive containing
"./sub/root -> .." extracted before this review round (tar.getmember(".")
found the member) and on upstream 26.2.2, but failed with InstallationError
after the member-name normalization change.
Record "." only when a member normalizes to it, never implicitly, which
matches the previous behaviour and upstream: without an explicit "." member
the link is still refused. No security impact: this only removes a false
rejection, and the realpath containment checks are unchanged (a link to the
root resolves to the extract directory itself, which is inside it).
Adds tests for the explicit-root (accepted) and implicit-root (refused)
cases.
Also restore the second blank line before
test_tar_member_paths_terminates_on_doubled_slash (flake8 E302), which the
previous review commit left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Nit addressed in
🤖 Generated with Claude Code |
ezequielp-activestate
left a comment
There was a problem hiding this comment.
Re-review of 02cb63b: 0 blockers. Approving.
The new commit fixes the nit from my last review. An explicit . / ./ member is now recorded as the archive root in _tar_member_paths (src/pip/_internal/utils/unpacking.py), so a symlink that resolves to it is accepted. The root is still never added implicitly. The source change is the same 6 lines on all five lines (pip PRs 4, 5, 6, 8 and 9).
Allowing . doesn't reopen the tar escape. I ran the exploit and legit-archive probe on this commit under Python 3.7, with tarfile.data_filter removed:
- All 7 escape shapes are still refused and nothing is written outside. That includes Case B (
.,redir -> .,redir/up -> ..,redir/up/outside.txt), which the realpath check now stops atredir/up. - Three extra variants that combine an explicit
.member with symlink chains (root -> .+root/up -> ..;sub/r -> ..+sub/r/../outside.txt;a -> .+a/a/a/b -> ..) are refused, with nothing written outside. - Legit archives L1–L4 still extract, and the new case (
.,./sub,./sub/root -> ..) now extracts.
Tests: tests/unit/test_utils_unpacking.py gives 67 passed, 1 skipped on Python 3.7. test_symlink_to_explicit_archive_root_is_in_tar fails on the previous code (assert '.' in {'sub', 'sub/root'}), checked on the 25.0.1 and 26.0.1 lines.
tests/unit suite (this commit only touches unpacking.py, whose tests were run).
…026.10.19.post1 (#4) * Backport CVE-2023-5752: prevent option injection via Mercurial revision GHSA-mq26-g339-26xf. Mercurial.get_base_rev_args returned the bare revision as its own argv element, so a revision such as --config=alias.update=!<cmd> reached hg as a standalone token and was parsed as a Mercurial command-line option, allowing configuration injection and code execution when installing from an hg URL. Return a single -r=<rev> token so hg always treats the whole value as the revision. Upstream: pypa#12306 (389cb79), released in 23.3. On 21.3.1 the method returned [rev] rather than upstream's ["-r", rev], so the fix also adds the -r flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Backport CVE-2026-1703: use os.path.commonpath in is_within_directory GHSA-6vgw-5pg2-w6jp. is_within_directory used os.path.commonprefix, which compares paths character by character, so a sibling such as /dest/parentfoo counted as within /dest/parent. A crafted archive could therefore write files into a sibling directory whose name has the install path as a string prefix. Switch to os.path.commonpath, which compares whole path components. Upstream: 4c651b7 (pypa#13777), released in 26.0. Applies as-is on Python 3.6. Adds upstream's prefix-substring case to test_is_within_directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Backport CVE-2025-8869: validate tar symlink targets during extraction GHSA-4xh5-x5gv-qwph. untar_file extracted symlink members without checking their target. A malicious sdist could ship a symlink pointing at an arbitrary host path, and later writes through that link would create or overwrite files outside the install directory. Upstream treats this as the fallback path for interpreters without tarfile.data_filter (PEP 706). Python 3.6 never has data_filter, so on 21.3.1 it is the only path. Add is_symlink_target_in_tar() and refuse any symlink member whose target is not itself a member of the same archive (InstallationError). Upstream: pypa#13550 (2490eb2..b154d06), released in 25.3. 21.3.1 has a single extraction loop, so the check goes there directly. Tests port upstream's four link tests without the data_filter monkeypatching, using TarInfo.linkname. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Backport CVE-2026-3219: disambiguate tar/zip archive detection in unpack_file GHSA-58qw-9mgm-455v. unpack_file ran zipfile.is_zipfile() before looking at the filename's tar extensions. is_zipfile scans for a ZIP end-of-central- directory record anywhere in the file, so a tar+ZIP polyglot (a ZIP appended to a valid .tar.gz) was always extracted as a ZIP, letting an attacker control which set of files pip actually installs. Choose the format in order of decreasing reliability (content-type, then filename extension, then magic signature) and only trust the magic signature when it is unambiguous. A file that is both a valid zip and a valid tar is rejected with InstallationError. Upstream: pypa#13870 (c4cdbf9), released in 26.1. Ported directly. Python 3.6 adaptations in the tests only: Optional[...] instead of PEP 604 unions, call_args[1] instead of call_args.kwargs (3.8+), and tmpdir/open() instead of tmp_path write_bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Backport CVE-2026-8643: reject entry point names that escape the scripts dir GHSA-wf93-45jw-7689. pip joined console_scripts / gui_scripts entry point names onto the scripts directory without checking that the result stays inside it. Add _script_within_dir() and have _raise_for_invalid_entrypoint() (called from PipScriptMaker.make with self.target_dir) raise InstallationError for any name that resolves outside the scripts dir. Upstream: pypa#14000 (8eb1784), released in 26.1.2. Ported as-is. Exposure on 21.3.1 is narrower than upstream: the vendored distlib 0.3.3 only accepts [\w.+-]+ entry point names, so a name with path separators (upstream's ../../outside payload) is reduced to its last component and stays in the scripts dir. The traversal came with distlib 0.4's looser name regex. On this line only '.' and '..' reach the join; they now fail with a clear InstallationError. Backported as defense in depth. Tests mirror upstream's, plus one pinning the distlib 0.3.3 separator behavior. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Backport CVE-2026-13346: stop Link.filename decoding the URL path twice GHSA-qwm4-qh6w-59xr. Link.path already percent-decodes the URL path, but Link.filename unquoted the basename a second time, so a doubly-encoded separator from a malicious index (..%252F..%252Fevil.whl) became a real "../../evil.whl" and the download was written outside the download directory. This matters most for `pip download --only-binary`. - Link.filename no longer decodes twice and always returns a single path component (no separator, never "." or ".."), falling back to the netloc. - New PathComponent NewType and helpers _to_path_component / as_path_component / join_within_directory in pip._internal.models.link. The HTTP response file name (including the Content-Disposition value, where a bare ".." previously got through sanitize_content_filename) is validated with as_path_component. - All four download-path joins go through join_within_directory: Downloader, BatchDownloader (21.3.1 has both), _check_download_dir and RequirementPreparer.save_linked_requirement. Upstream: pypa#14110 (10dfb6b), released in 26.2.0. Adaptation: NewType is only enforced by mypy, so join_within_directory also re-validates its argument at runtime and raises ValueError, stricter than upstream and the same as the 20.3.4.post2 backport. Tests mirror upstream's test_link.py additions, plus download-level tests for the doubly-encoded URL and Content-Disposition "..". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Backport CVE-2026-6357: run the self-version check before installing GHSA-jp4c-xjxw-mgf9. pip ran its self-version check in a `finally` after the command body, so after `pip install` had already written wheels into site-packages. The check queries the environment and builds a PackageFinder, and the vendored packaging.tags then probes `import _manylinux` (the PEP 513/600 override hook, not lru_cached). A just-installed wheel that ships a top-level `_manylinux.py` was therefore imported and executed by pip itself. Reproduced on pristine 21.3.1 under Python 3.6 with a wheel containing only `_manylinux.py`. A sys.modules trace across the post-install check showed nothing, because a failed `import _manylinux` is never stored in sys.modules; the earlier "not applicable" assessment was wrong. Split the check into a fetch phase, which runs before the command body (environment lookup, state file, remote lookup, "installed by pip" decision), and an emit phase, which only logs the prompt afterwards: - Command.handle_pip_version_check(options) becomes the context manager Command.pip_version_check(options, args) and wraps the command body in Command._main. - IndexGroupCommand fetches before yielding and emits in `finally`, each guarded by upstream's "error checking the latest version" handling. - InstallCommand skips the check when pip itself is a requirement. - self_outdated_check gains UpgradePrompt, pip_self_version_check_fetch and pip_self_version_check_emit. The installer is read from the dist found during fetch instead of re-querying the environment afterwards, so was_installed_by_pip is removed, as upstream. Upstream: pypa#13923 (b369bfc), released in 26.1. Follows the ActiveState 24.0 port (bedea55). Adaptation: 21.3.1 has no index_command.py, _run_wrapper or rich-rendered UpgradePrompt, so the context manager lives in cli/req_command.py and wraps run() in _main. UpgradePrompt is a NamedTuple (no dataclasses on Python 3.6), and emit keeps 21.3.1's existing warning text. pip_self_version_check(session, options) is kept as a fetch+emit wrapper so the old entry point still works. 21.3.1 has no externally-managed check, so that part of upstream does not apply. Tests follow upstream, plus fetch -> body -> emit ordering and a test that emit never touches the environment or PackageFinder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Upgrade vendored certifi to 2026.10.19.post1 The vendored certifi 2021.05.30 still trusts root certificates Mozilla has since removed for cause: - CVE-2022-23491 (GHSA-43fp-rhv2-5gv8): TrustCor RootCert CA-1, CA-2, ECA-1 - CVE-2023-37920 (GHSA-xqr8-7jwr-rhp7): E-Tugra Certification Authority - CVE-2024-39689 (GHSA-248v-346w-9cwc): GLOBALTRUST 2020 (added in certifi 2021.05.30, so this line is affected) - GHSA-6q48-6c7w-9gw2 (no CVE): emSign ECC Root CA - G3, whose intermediate CA keys were compromised Replace the vendored copy with ActiveState certifi 2026.10.19.post1 (ActiveState/certifi 87fbc73). That is upstream certifi 2026.10.19, the release vendored by pip 26.2.2, with a pre-3.7 fallback. Python 3.6 has no importlib.resources, so upstream 2026.10.19 cannot be vendored as-is on this line. cacert.pem, __init__.py and LICENSE are byte-identical to that release; __main__.py gets pip's usual import rename. core.py keeps both of pip's vendoring patches: the "certifi" -> "pip._vendor.certifi" resource rename, and the _PIP_STANDALONE_CERT override that build_env.py relies on so the standalone pip in an isolated build environment uses the parent's CA bundle. The override now also defines contents(); the old patch skipped read_text() in that branch, so contents() raised NameError there. tools/vendoring/patches/certifi.patch was regenerated and reproduces the vendored core.py from the release. Checked on Python 3.6, 3.9 and 3.12 (all three core.py branches) and with _PIP_STANDALONE_CERT set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Release 21.3.1.post1: backport seven pip CVEs; vendored certifi 2026.10.19.post1 Bump version 21.3.1 -> 21.3.1.post1 (PEP 440 post-release) and add a NEWS section covering CVE-2023-5752, CVE-2025-8869, CVE-2026-1703, CVE-2026-3219, CVE-2026-6357, CVE-2026-8643 and CVE-2026-13346, plus the vendored certifi upgrade (CVE-2022-23491, CVE-2023-37920, CVE-2024-39689, GHSA-6q48-6c7w-9gw2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: harden CVE-2025-8869 fix against symlink traversal (pypa#14127) Reviewer blocker on PR #4: the CVE-2025-8869 backport took upstream's 25.3 fix but not the later hardening in pypa#14127 (17eb176, pip 26.2). Python 3.6 has no tarfile.data_filter, so untar_file's member loop is the only guard for every user of this line, and two archive shapes still wrote outside the extract directory: - symlink outside_link -> .., then outside_link/outside.txt (the error only came at a later ".." member, after the write); - ".", redir -> ., redir/up -> .., redir/up/outside.txt (no error at all). is_symlink_target_in_tar only asks whether the link target is an archive member, and "." / ".." are members; the per-member check used abspath. Port the upstream fix: is_within_directory gains a keyword-only resolve_symlinks flag (realpath instead of abspath); each member is also checked with symlinks resolved, and a symlink whose target resolves outside the destination is rejected before it is created. Upstream's two tests are ported unchanged (they force the fallback where data_filter exists). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: root-safe path containment for is_within_directory and _script_within_dir Two reviewer should-fix findings on PR #4: - is_within_directory used os.path.commonpath, which drops a POSIX "//" prefix: is_within_directory("//srv/env/bin", "//srv/env/bin/pip") was False, so `pip install --prefix //tmp/pfx <wheel>` failed with "trying to install outside the target directory". On Windows commonpath also raised ValueError for a target on another drive. (Upstream fixed the "//" case in 7ff8bdd, but with a form that rejects everything under "/".) - _script_within_dir used dest.startswith(root + os.sep), so with a scripts directory of "/" or "C:\" every entry point was rejected. Compare on a component boundary that is root-safe: the target equals the directory, or starts with the directory plus a separator (a root already ends with one). _script_within_dir reuses is_within_directory and still rejects the directory itself (".", ".."), so the CVE-2026-8643 and CVE-2026-1703 cases keep failing closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: pass Mercurial revisions as --rev=<rev> (pypa#12373) Reviewer blocker on PR #4: the CVE-2023-5752 backport emitted `-r=<rev>`, which is upstream's first fix (pypa#12306) and broke Mercurial: hg parses `-r=0` as the revset `=0` and fails with "hg: parse error at 0: not a prefix: =". Every `hg+...@<rev>` install (obtain, switch and update all build their command line this way) failed. Port upstream's follow-up pypa#12373 (6dbd9c6, pip 23.3.1): use the long form `--rev=<rev>`. It is still a single token, and hg reads `--rev=--config=...` as a revision value, not an option, so the CVE-2023-5752 protection holds. Update the two tests that asserted the broken form. Verified end to end with Mercurial 7.0.1: `pip install "hg+file:///repo@0#egg=demo"` installs revision 0 with this change and fails with the previous `-r=` form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: run the pre-command version check inside pip's error handling Reviewer finding (#3 should-fix, #5 nit; same code on this line): the CVE-2026-6357 port wrapped the already-intercepted `run` in `with self.pip_version_check(...)`, so the pre-command fetch ran outside intercepts_unhandled_exc. A ValueError from `_arg_refers_to_pip` (urlparse's "Invalid IPv6 URL" for `pip install "foo @ http://[bad/x"`) or a Ctrl-C during the fetch escaped as a raw, uncaught traceback (exit 1). Move the `with` into an inner function that is itself wrapped by intercepts_unhandled_exc, which is where upstream has it (`_inner_run`). Also let `_arg_refers_to_pip` treat a bare ValueError like InvalidRequirement (not pip), so the malformed requirement reaches the normal requirement parser. `pip install "foo @ http://[bad/x"` now behaves exactly as on pristine 21.3.1 (pip's "ERROR: Exception:" report, exit 2); a Ctrl-C during the fetch prints "Operation cancelled by user". Tests cover both, plus _arg_refers_to_pip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: accept normalized member names in is_symlink_target_in_tar Reviewer should-fix on PR #4: is_symlink_target_in_tar looked the link target up with tar.getmember(), which needs the exact member name. Two valid archive shapes were therefore refused on Python 3.6, where this tar fallback is the only extraction path (they unpack on pristine 21.3.1): - a "./"-prefixed archive (as made by `tar -C dir .`) with ./pkg/link -> target.txt; - a symlink to a directory that has no directory entry of its own. Compare against the set of normalized member paths plus every implied parent directory instead. The set is built once per archive and passed in, so a large archive with many symlinks stays linear. Upstream 26.2.2 still refuses both shapes; this is a deliberate improvement. It does not weaken containment: the symlink-resolving is_within_directory checks from the CVE-2025-8869 hardening reject any link or member that resolves outside the destination, and a link to a path that is not in the archive is still refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: report unsafe download file names as a pip error, not a traceback Reviewer nit (all five PRs): a download whose name reduces to "." / ".." (e.g. `Content-Disposition: filename=".."` with a content type that has no known extension) was correctly refused by as_path_component / join_within_directory, but as a bare ValueError, so pip printed "ERROR: Exception:" and a traceback (exit 2). Add UnsafeFilenameError(InstallationError, ValueError) and raise it instead. It is still a ValueError, so existing callers and tests keep working, and pip's command loop now reports it as a normal error line: ERROR: Unexpected file name derived from URL: '..' (exit 1) Verified end to end against a local server sending that header. The write is still blocked either way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: NEWS — render notes, record review fixes - The release section's `.. note` (no `::`) was an RST comment and did not render (reviewer nit on #3/#7; same pattern here). It is now a `.. note::` directive. The upstream header comment on line 1 is left as is. - Record the review fixes in the 21.3.1.post1 section: the CVE-2025-8869 hardening (pypa#14127), the Mercurial `--rev=` form for the CVE-2023-5752 fix (pypa#12373), the component-boundary wording for CVE-2026-1703, and a Bug Fixes list for root-safe path checks, the version check's error handling, normalized tar symlink lookup and the clean error for unsafe download names. Checked with docutils: no warnings and the note renders as an admonition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: stop _tar_member_paths at a fixed point posixpath.normpath keeps a leading "//" and posixpath.dirname("//") is "//", so collecting the implied parent directories of a member named "//x/y" never terminated: a crafted archive could hang pip in the tar fallback path. Stop walking parents once dirname() stops changing the name, and add a regression test. The bug was in the shared review-round helper (item G) and was caught on the 26.0.1 line; this applies the same guard here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: let a symlink resolve to an explicit archive root _tar_member_paths never recorded ".", so is_symlink_target_in_tar refused a symlink that resolves to the archive root even when the archive has an explicit "." member. A "tar -C dir ." archive containing "./sub/root -> .." extracted before this review round (tar.getmember(".") found the member) and on upstream 26.2.2, but failed with InstallationError after the member-name normalization change. Record "." only when a member normalizes to it, never implicitly, which matches the previous behaviour and upstream: without an explicit "." member the link is still refused. No security impact: this only removes a false rejection, and the realpath containment checks are unchanged (a link to the root resolves to the extract directory itself, which is inside it). Adds tests for the explicit-root (accepted) and implicit-root (refused) cases. Also restore the second blank line before test_tar_member_paths_terminates_on_doubled_slash (flake8 E302), which the previous review commit left out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Second ActiveState security release for the pip 24.0 line (Python 3.7), on top of the
24.0.0.1release (CVE-2025-8869 and CVE-2026-1703).Versioning:
24.0.post2. The 24.0.0.1 build kept its internal version at24.0, so it counts as release #1. That makes the override-table version24.0.0.2, which doesn't collide with24.0.0.1. Under PEP 440,24.0.post2sorts below the string24.0.0.1, but no artifact ever reported that version (its wheel andpip --versionsay24.0), so upgrades are unaffected.49b93c48c(upstream pypa#13870 as-is)bedea55d8507ce6d8f, a real fix (distlib 0.3.8 already has the loose regex; pristine wrote../../../../tmp/escaped-script)d3bef733ef3e1264dc(GLOBALTRUST)Notes
tests/. The older TrustCor and E-Tugra CVEs were already fixed by 2023.7.22.Command.handle_pip_version_checkbecomes thepip_version_check(options, args)context manager (internal API).pip_self_version_check()remains as a wrapper.Validation (
python:3.7-slim)tests/unitgives 1590 passed, against 1497 on pristinePython-3.7-compat, with the same 4 git-dependent failures. The new tests fail on pristine._manylinux.pyis installed with the version check enabled. The vendored packaging retriesimport _manylinuxon every tag computation, because a failed import is never cached. Pristine pip runs the malicious file after the install; this build does not.24.0.post2with certifi 2026.10.19, and realpip downloadandpip install six==1.16.0work over TLS. black is clean and ruff counts are unchanged.🤖 Generated with Claude Code