Skip to content

pip 26.0.1.post1 follow-up: let a symlink resolve to an explicit archive root - #9

Merged
icanhasmath merged 1 commit into
26.0.1.xfrom
26.0.1-security-rootlink
Oct 22, 2026
Merged

icanhasmath merged 1 commit into
26.0.1.xfrom
26.0.1-security-rootlink

Conversation

@icanhasmath

Copy link
Copy Markdown

Follow-up to #7, which merged before this review nit was addressed. It is not a new release: 26.0.1.post1 is not tagged yet, so this lands in the same version.

The nit: _tar_member_paths never recorded ".", so is_symlink_target_in_tar refused a symlink resolving to the archive root even when the archive has an explicit . member. A tar -C dir . archive containing ./sub/root -> .. extracted before the review round and on upstream 26.2.2, but failed with InstallationError after the member-name normalization change.

The fix: record "." only when a member normalizes to it, never implicitly. That matches the earlier behaviour and upstream: without an explicit . member, the link is still refused. There is no security impact, since this only removes a false rejection and the realpath containment checks are unchanged.

Verified:

Archive Before the review round Merged head This PR Upstream 26.2.2
explicit ./ + ./sub/root -> .. ok refused ok ok
no ./ member, sub/root -> .. refused refused refused refused
  • The Fix symlink path traversal in untar fallback pypa/pip#14127 attack archives are still refused (escaped=False).
  • The ./-prefixed, implied-directory and ordinary-symlink archives still unpack.
  • test_utils_unpacking.py passes (68 passed, Python 3.9), and the new explicit-root test fails with the fix reverted.

The same commit has gone onto the three open PRs (#4, #5, #6).

🤖 Generated with Claude Code

_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.

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.

Follow-up review (40f50f2): 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.9, 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 at redir/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 68 passed, 1 skipped on Python 3.9. 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.

⚠️ Not assessed: Windows, and the full tests/unit suite (this commit only touches unpacking.py, whose tests were run).

@icanhasmath
icanhasmath merged commit c9585e3 into 26.0.1.x Oct 22, 2026
25 checks passed
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