VAITP Dataset

← Back to the dataset

CVE-2026-54591

Path traversal in AsyncSSH SCP client allows arbitrary file writes.

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

AsyncSSH is a Python package which provides an asynchronous client and server implementation of the SSHv2 protocol on top of the Python asyncio framework. Prior to 2.23.1, a malicious SSH server can write arbitrary files on the asyncssh SCP client's filesystem by sending filenames containing ../ traversal sequences because _parse_cd_args in scp.py returns server-provided names verbatim and _recv_files joins them to the destination path without enforcing the target directory boundary. This issue is fixed in version 2.23.1.

CVSS base score
8.1
Published
2026-07-08
OWASP
A03 Injection
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
AsyncSSH
Fixed by upgrading
Yes

Solution

Upgrade `asyncssh` to version 2.23.1 or later.

Vulnerable code sample

import os
import shutil

# This function simulates the vulnerable logic in asyncssh's scp.py before
# the fix. The core of the vulnerability is that a server-provided filename
# containing path traversal sequences ('../') is joined to a local path
# without being sanitized first.
def vulnerable_file_reception_logic(local_dest_dir, server_provided_filename, file_content):
    # The vulnerable operation: os.path.join processes the '../' sequence
    # from the untrusted 'server_provided_filename', allowing it to "escape"
    # the 'local_dest_dir'.
    final_path = os.path.join(local_dest_dir, server_provided_filename)

    # In a real scenario, the library would ensure parent directories exist.
    # This allows the attack to work even if the target path doesn't exist.
    os.makedirs(os.path.dirname(final_path), exist_ok=True)

    # The file is written to the traversed path, outside the intended directory.
    with open(final_path, 'w') as f:
        f.write(file_content)
    
    print(f"File write attempted to: {os.path.abspath(final_path)}")


# --- Demonstration of the vulnerability ---

# 1. Define the directory where the user intends to save the file.
intended_safe_dir = "downloads"

# 2. This is the malicious filename a compromised server would send.
# It uses '../' to traverse up one level from the intended directory.
malicious_filename = "../pwned_file.txt"

# 3. Clean up any artifacts from previous runs to ensure a clean test.
if os.path.exists(intended_safe_dir):
    shutil.rmtree(intended_safe_dir)
if os.path.exists("pwned_file.txt"):
    os.remove("pwned_file.txt")

# 4. Create the intended "safe" directory.
os.makedirs(intended_safe_dir)
print(f"Intended download directory created at: {os.path.abspath(intended_safe_dir)}")

# 5. Execute the vulnerable logic, simulating a file reception.
vulnerable_file_reception_logic(
    intended_safe_dir,
    malicious_filename,
    "This file was written outside the 'downloads' directory."
)

# 6. Verify the outcome.
print("-" * 20)
if os.path.exists("pwned_file.txt"):
    print("VULNERABILITY CONFIRMED: A file was created outside the intended directory.")
    print(f"Location: {os.path.abspath('pwned_file.txt')}")
else:
    print("Vulnerability could not be demonstrated.")

if not os.listdir(intended_safe_dir):
    print("The intended 'downloads' directory is correctly empty.")

# 7. Final cleanup.
shutil.rmtree(intended_safe_dir)
os.remove("pwned_file.txt")

Patched code sample

import os

def get_safe_path(dest_dir, filename):
    """
    Safely join a destination directory and a filename, preventing path traversal.

    This function represents the logic used to fix the path traversal vulnerability
    by ensuring the final resolved path is within the intended destination directory.
    """

    # Resolve the destination directory to its absolute, canonical path
    real_dest_dir = os.path.realpath(dest_dir)

    # Create the proposed full path
    proposed_path = os.path.join(real_dest_dir, filename)

    # Resolve the proposed path to its absolute, canonical path.
    # This will interpret any '..' traversal sequences.
    real_proposed_path = os.path.realpath(proposed_path)

    # Check if the common path of the destination directory and the proposed
    # file path is the destination directory itself. If not, the path
    # has escaped the intended directory.
    if os.path.commonpath([real_dest_dir, real_proposed_path]) != real_dest_dir:
        raise ValueError(f"Path traversal attempt detected: '{filename}'")

    return real_proposed_path

Payload

../../../../tmp/pwned.txt

Cite this entry

@misc{vaitp:cve202654591,
  title        = {{Path traversal in AsyncSSH SCP client allows arbitrary file writes.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-54591},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-54591/}}
}
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 ::