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.
- CWE
- CWE-22
- 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_pathPayload
../../../../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 ::
