PYSEC-2026-3808
Vulnerability from pysec - Published: 2026-09-10 09:44 - Updated: 2026-09-10 11:01| Product | asyncssh (all versions through 2.23.0) |
| Related | CVE-2019-6111 (same class in OpenSSH) |
| Fix | AsyncSSH 2.23.1 |
A malicious SSH server can write arbitrary files on the asyncssh SCP client's filesystem by sending filenames containing ../ traversal sequences. The SCP receive path does not currently sanitize server-provided filenames. By chaining directory traversals via the D (directory) action, an attacker can escape any target directory and overwrite ~/.bashrc, ~/.ssh/rc, or ~/.ssh/authorized_keys, achieving code execution. This is the same vulnerability class as CVE-2019-6111. The mitigation applied in OpenSSH does not appear to have been adopted in asyncssh.
Steps to exploit:
Step 1 - Normal usage: Application calls await asyncssh.scp((conn, 'file'), '/home/user/downloads/'). This is the standard, documented API.
Step 2 - SCP protocol: asyncssh opens an SSH exec channel, runs scp -f file. The server controls the filename field:
C0644 100 ../pwned.txt\n (simple traversal)
D0755 0 ..\n (traverse up, repeat as needed)
C0644 47 .bashrc\n (write payload)
E\n
Step 3 - _parse_cd_args (scp.py:134-142) returns the filename verbatim:
def _parse_cd_args(args: bytes) -> Tuple[int, int, bytes]:
permissions, size, name = args.split(None, 2)
return int(permissions, 8), int(size), name # no sanitization
The returned name is not passed through basename() and is not checked for .. or / components.
Step 4 - _recv_files (scp.py:706-713) joins the unsanitized name:
new_dstpath = posixpath.join(dstpath, name)
With dstpath=b'/home/user/downloads/subdir' and name=b'../pwned.txt', this resolves to /home/user/downloads/pwned.txt, outside the target.
Step 5 - File write: _recv_file opens the traversed path via self._fs.open(dstpath, 'wb') and writes attacker-controlled content. The resolved path is not checked against the target directory boundary.
Step 6 - RCE chains:
| Target | Execution trigger | Reliability |
|---|---|---|
~/.bashrc |
Next terminal open | High |
~/.profile |
Next login | High |
~/.ssh/rc |
Next SSH connection (requires sshd) | High |
~/.ssh/authorized_keys |
Attacker logs in with command= |
Medium |
Reproduction:
Link to reproduction script: path_traversal_poc.zip
docker build -t asyncssh-scp-traversal -f Dockerfile .
docker run --rm asyncssh-scp-traversal
The attached poc_scp_traversal.py starts a malicious SSH server in-process using asyncssh's own API, then downloads from it via asyncssh.scp().
Expected Output:
| Name | purl | asyncssh | pkg:pypi/asyncssh |
|---|
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "asyncssh",
"purl": "pkg:pypi/asyncssh"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.23.1"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.8.1",
"0.8.2",
"0.8.3",
"0.8.4",
"0.9.0",
"0.9.1",
"0.9.2",
"1.0.0",
"1.0.1",
"1.1.0",
"1.1.1",
"1.10.0",
"1.10.1",
"1.11.0",
"1.11.1",
"1.12.0",
"1.12.1",
"1.12.2",
"1.13.0",
"1.13.1",
"1.13.2",
"1.13.3",
"1.14.0",
"1.15.0",
"1.15.1",
"1.16.0",
"1.16.1",
"1.17.0",
"1.17.1",
"1.18.0",
"1.2.0",
"1.2.1",
"1.3.0",
"1.3.1",
"1.3.2",
"1.4.0",
"1.4.1",
"1.5.0",
"1.5.1",
"1.5.2",
"1.5.3",
"1.5.4",
"1.5.5",
"1.5.6",
"1.6.0",
"1.6.1",
"1.6.2",
"1.7.1",
"1.7.2",
"1.7.3",
"1.8.0",
"1.8.1",
"1.9.0",
"2.0.0",
"2.0.1",
"2.1.0",
"2.10.0",
"2.10.1",
"2.11.0",
"2.12.0",
"2.13.0",
"2.13.1",
"2.13.2",
"2.14.0",
"2.14.1",
"2.14.2",
"2.15.0",
"2.16.0",
"2.17.0",
"2.18.0",
"2.19.0",
"2.2.0",
"2.2.1",
"2.20.0",
"2.21.0",
"2.21.1",
"2.22.0",
"2.23.0",
"2.3.0",
"2.4.0",
"2.4.1",
"2.4.2",
"2.5.0",
"2.6.0",
"2.7.0",
"2.7.1",
"2.7.2",
"2.8.0",
"2.8.1",
"2.9.0"
]
}
],
"aliases": [
"CVE-2026-54591",
"GHSA-2wxc-x7rj-hg8f"
],
"details": "| | |\n|---|---|\n| Product | asyncssh (all versions through 2.23.0) |\n| Related | CVE-2019-6111 (same class in OpenSSH) |\n| Fix | AsyncSSH 2.23.1 |\n\nA malicious SSH server can write arbitrary files on the asyncssh SCP client\u0027s filesystem by sending filenames containing `../` traversal sequences. The SCP receive path does not currently sanitize server-provided filenames. By chaining directory traversals via the `D` (directory) action, an attacker can escape any target directory and overwrite `~/.bashrc`, `~/.ssh/rc`, or `~/.ssh/authorized_keys`, achieving code execution. This is the same vulnerability class as CVE-2019-6111. The mitigation applied in OpenSSH does not appear to have been adopted in asyncssh.\n\n---\n\n**Steps to exploit:**\n\n**Step 1 - Normal usage:** Application calls `await asyncssh.scp((conn, \u0027file\u0027), \u0027/home/user/downloads/\u0027)`. This is the standard, documented API.\n\n**Step 2 - SCP protocol:** asyncssh opens an SSH exec channel, runs `scp -f file`. The server controls the filename field:\n\n```\nC0644 100 ../pwned.txt\\n (simple traversal)\n\nD0755 0 ..\\n (traverse up, repeat as needed)\nC0644 47 .bashrc\\n (write payload)\nE\\n\n```\n\n**Step 3 - `_parse_cd_args`** (`scp.py:134-142`) returns the filename verbatim:\n\n```python\ndef _parse_cd_args(args: bytes) -\u003e Tuple[int, int, bytes]:\n permissions, size, name = args.split(None, 2)\n return int(permissions, 8), int(size), name # no sanitization\n```\n\nThe returned `name` is not passed through `basename()` and is not checked for `..` or `/` components.\n\n**Step 4 - `_recv_files`** (`scp.py:706-713`) joins the unsanitized name:\n\n```python\nnew_dstpath = posixpath.join(dstpath, name)\n```\n\nWith `dstpath=b\u0027/home/user/downloads/subdir\u0027` and `name=b\u0027../pwned.txt\u0027`, this resolves to `/home/user/downloads/pwned.txt`, outside the target.\n\n**Step 5 - File write:** `_recv_file` opens the traversed path via `self._fs.open(dstpath, \u0027wb\u0027)` and writes attacker-controlled content. The resolved path is not checked against the target directory boundary.\n\n**Step 6 - RCE chains:**\n\n| Target | Execution trigger | Reliability |\n|---|---|---|\n| `~/.bashrc` | Next terminal open | High |\n| `~/.profile` | Next login | High |\n| `~/.ssh/rc` | Next SSH connection (requires sshd) | High |\n| `~/.ssh/authorized_keys` | Attacker logs in with `command=` | Medium |\n\n---\n\n**Reproduction:**\n\nLink to reproduction script: [path_traversal_poc.zip](https://github.com/user-attachments/files/28160665/path_traversal_poc.zip)\n\n```bash\ndocker build -t asyncssh-scp-traversal -f Dockerfile .\ndocker run --rm asyncssh-scp-traversal\n```\n\nThe attached `poc_scp_traversal.py` starts a malicious SSH server in-process using asyncssh\u0027s own API, then downloads from it via `asyncssh.scp()`.\n\n*Expected Output:*\n\n\u003cimg width=\"1400\" height=\"815\" alt=\"image\" src=\"https://github.com/user-attachments/assets/496745e4-d11d-4ed8-bddd-d15dd13d1751\" /\u003e",
"id": "PYSEC-2026-3808",
"modified": "2026-09-10T11:01:58.237758Z",
"published": "2026-09-10T09:44:57.655635Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/ronf/asyncssh/security/advisories/GHSA-2wxc-x7rj-hg8f"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54591"
},
{
"type": "WEB",
"url": "https://github.com/ronf/asyncssh/commit/d730803b8e4e94c20c7580d90f94d1e05f9f58de"
},
{
"type": "PACKAGE",
"url": "https://github.com/ronf/asyncssh"
},
{
"type": "WEB",
"url": "https://github.com/ronf/asyncssh/releases/tag/v2.23.1"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/asyncssh"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-2wxc-x7rj-hg8f"
}
],
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "asyncssh has SCP Path Traversal to Arbitrary File Write"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.