Skip to content

Validate tar link targets in data_filter fallback (GHSA-p4qx-p8p6-4gjf) - #6654

Merged
matteius merged 6 commits into
mainfrom
fix/ghsa-p4qx-p8p6-4gjf-tar-traversal
Apr 25, 2026
Merged

Validate tar link targets in data_filter fallback (GHSA-p4qx-p8p6-4gjf)#6654
matteius merged 6 commits into
mainfrom
fix/ghsa-p4qx-p8p6-4gjf-tar-traversal

Conversation

@matteius

Copy link
Copy Markdown
Member

Summary

Fixes GHSA-p4qx-p8p6-4gjf: a tar hardlink/symlink traversal in pipenv.patched.pip._internal.utils.unpacking.untar_file.

When tarfile.data_filter raised LinkOutsideDestinationError for a link member, untar_file fell back to tarfile.tar_filter on three specific CPython patch versions (3.9.17 / 3.10.12 / 3.11.4) that shipped with python/cpython#107845. The link-target containment check in CPython's _get_filtered_attrs lives inside the if for_data: branch, so tar_filter performs no containment validation. An attacker-controlled tarball with a hardlink whose linkname escapes the destination (e.g. ../../etc-passwd-shadow) would have its link materialized outside the install root, enabling cache pollution / overwrites of files outside the destination.

  • New helper _tar_link_target_is_within(member, destination) re-implements the containment check data_filter is supposed to perform — resolves the link target relative to the destination (or, for symlinks, relative to the link's own directory), then confirms it stays inside via os.path.commonpath.
  • The fallback in pip_filter now only routes to tar_filter when (a) we're on a buggy CPython patch version and (b) the link target is independently confirmed in-bounds. Otherwise the original LinkOutsideDestinationError is re-raised and untar_file wraps it in InstallationError, so the install fails closed.
  • The change is mirrored in tasks/vendoring/patches/patched/pip_tarfile_link_safety.patch so it survives a re-vendor of pip.

Test plan

  • New tests/unit/test_tar_link_safety.py (11 tests):
    • 8 unit tests for _tar_link_target_is_within — accepts in-bounds hardlinks/symlinks; rejects ../ traversal, deep ../../../ traversal, absolute paths, empty linknames, and non-link members.
    • 3 end-to-end tests on real tar archives, monkeypatching data_filter to simulate the buggy CPython behaviour: malicious hardlink rejected (no link materialised on disk, InstallationError raised); legitimate in-bounds hardlink still extracts; modern-CPython path also rejects malicious hardlink before the fallback runs.
  • python -m pytest tests/unit -q — 434 passed, 0 new failures (one pre-existing unrelated failure on main).
  • ruff check clean on the touched files.
  • git apply --check confirmed pip_tarfile_link_safety.patch is byte-identical to the working-tree diff.

🤖 Generated with Claude Code

The ``LinkOutsideDestinationError`` fallback in ``untar_file`` routed
through ``tarfile.tar_filter``, which does not perform link-target
containment checks (those live inside the ``for_data`` branch of
CPython's ``_get_filtered_attrs``). On the three CPython patch versions
the workaround targets, attacker-controlled hardlink/symlink members
whose targets escaped the destination directory were silently allowed,
permitting writes outside the install root.

The fallback now only runs when an independent containment check
confirms the link target stays inside the destination — otherwise it
fails closed and the extraction is aborted. The check is also captured
in tasks/vendoring/patches/patched/ so it survives a re-vendor of pip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses GHSA-p4qx-p8p6-4gjf by ensuring that when tarfile.data_filter triggers the historical LinkOutsideDestinationError fallback on specific buggy CPython patch versions, link targets are independently validated to remain within the extraction destination before falling back to tarfile.tar_filter.

Changes:

  • Add _tar_link_target_is_within() to re-check hardlink/symlink containment prior to using the permissive tar_filter fallback.
  • Tighten the LinkOutsideDestinationError fallback logic to only allow fallback on affected CPython patch versions and only for in-bounds link targets; otherwise fail closed.
  • Add a vendoring patch to preserve the fix across pip re-vendoring, plus new unit/regression tests for the helper and end-to-end extraction behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
pipenv/patched/pip/_internal/utils/unpacking.py Adds link-target containment helper and gates the buggy-CPython tar_filter fallback on the helper returning in-bounds.
tests/unit/test_tar_link_safety.py Introduces regression tests for helper behavior and extraction behavior under simulated buggy data_filter.
tasks/vendoring/patches/patched/pip_tarfile_link_safety.patch Mirrors the unpacking.py changes as a vendoring patch to make the fix durable across updates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/unit/test_tar_link_safety.py Outdated
Comment thread tests/unit/test_tar_link_safety.py Outdated
Comment thread tests/unit/test_tar_link_safety.py
Comment thread tests/unit/test_tar_link_safety.py Outdated
matteius and others added 4 commits April 25, 2026 01:40
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@matteius
matteius merged commit e7fa9b0 into main Apr 25, 2026
22 checks passed
@matteius
matteius deleted the fix/ghsa-p4qx-p8p6-4gjf-tar-traversal branch April 25, 2026 07:43
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.

3 participants