GitHub Advisoryadvisory · PyPI

GHSA-qr67-gv47-xwwh asyncssh has an incomplete fix for CVE-2026-45309 — AuthorizedKeysFile %u still escapes the intended directory via a leading ~ (and weakly via ${ENV}) username substitution

MEDIUM Published 2026-08-26 Modified 2026-08-26 Official page ↗
Aliases
CVE-2026-54590
Ecosystem
PyPI
Packages
asyncssh
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N
Weaknesses
CWE-22, CWE-639
Reviewed
2026-08-26

Affected packages

• PyPI asyncssh: < 2.23.1 → fixed in 2.23.1

Description

Incomplete fix for CVE-2026-45309 (GHSA-g794-3fmp-753h). The 2.23.0 guard that sanitises the SSH username before `%u` substitution in `AuthorizedKeysFile` blocks `/`, `\` and `..`, but does not block a leading `~` (or `${ENV}`), both of which are re-introduced by later expansion and reach the file open — defeating the guard. Affected: asyncssh 2.23.0 and current `develop` (commit `a60f863`, HEAD on 2026-05-29). ## Summary The fix for CVE-2026-45309 added a guard in `SSHServerConfig._set_tokens` (`asyncssh/config.py:715-716`) that rejects an SSH username containing `/`, `\`, or equal to `..`, before it is substituted for the `%u` token in `AuthorizedKeysFile`: if self._user == '..' or '/' in self._user or '\\' in self._user: raise IllegalUserName('Unsafe username substitution') However, the `%u`-substituted value is subsequently passed through environment-variable expansion (`_expand_val`, `config.py:145-149` — token expansion then env expansion) and, at file-open time, through `expanduser()` (`read_authorized_keys` → `read_file` → `open(Path(filename).expanduser())`, `auth_keys.py:348` → `misc.py:290`). Both re-introduce the path control the guard was meant to remove, so a username that contains no `/`/`\` can still cause the server to read an authorized-keys file outside the intended per-user directory. The client-supplied username reaches this path pre-authentication: `_process_userauth_request` takes the username from the `SSH_MSG_USERAUTH_REQUEST` packet (`connection.py:2516-2519`) and `_finish_userauth` calls `reload_config()` (`connection.py:2536`), which re-evaluates `AuthorizedKeysFile` with `username=self._username` (`connection.py:5906`) before the offered key is validated. ## Primary vector — leading `~` A username such as `~root` or `~victim` passes the guard (no `/`). For a server whose `AuthorizedKeysFile` begins with `%u` — e.g. `AuthorizedKeysFile %u/.ssh/authorized_keys` — the expanded value is `~victim/.ssh/authorized_keys`, which `expanduser()` resolves to `/home/victim/.ssh/authorized_keys` (`~root` → `/root/...`; a bare `~` → the server process's home). The username has therefore escaped the intended per-user location without using any path separator — defeating the purpose of the guard. Note: `expanduser()` only expands a leading `~`, so this vector requires `%u` to be the first path component of `AuthorizedKeysFile`. (The CVE-2026-45309 `authorized_keys/%u` example — `%u` not leading — is not reachable this way; that was the `../` form.) ## Impact and limitations • Demonstrated (verified against source at `a60f863`): the guard is bypassable and the authorized-keys lookup is redirected to an attacker-named home tree, pre-auth, with a separator-free username. • Impact model = identical to CVE-2026-45309: authenticating as the redirected username when a readable authorized-keys file containing the attacker's key is reachable at the redirected location. The parent CVE accepted this exact precondition and was scored `C:N/I:H/A:N`; this is scored consistently. • Not built: a live multi-account SSH auth harness; the PoC verifies the path-redirection mechanism in-process, deterministically. No new primitive is claimed beyond the parent CVE's accepted model — only that the 2.23.0 fix does not close it for `~`/`${ENV}`. • Preconditions (captured by AC:H): `%u` must be the leading path component; on Python 3.13, `Path('~nonexistentuser').expanduser()` raises `RuntimeError`, so only existing accounts are reachable (confirmed: asyncssh 2.23.0, Python 3.13.12). ## Secondary vector — `${ENV}` (defense-in-depth only) A username like `${HOME}` also passes the guard and is then environment-expanded, re-introducing `/`. Weaker and not a practical exploit: the attacker can only reference env vars that already exist in the server process (a missing variable raises `ConfigParseError`) and cannot control their values. Reported as hardening. ## Reproduction In-process, deterministic, no network. Against a checkout of asyncssh 2.23.0: cd /path/to/asyncssh PYTHONPATH=/path/to/asyncssh python3 poc_authkeys_token_bypass.py Output (abridged): [1] original CVE '../../../../tmp/evil' blocked: True (fix present) literal-slash user '/etc' blocked: True [A] tilde bypass user '~root': guard blocks it? False (False == bypass) expanded config value : ['~root/.ssh/authorized_keys'] after expanduser() : /root/.ssh/authorized_keys """ import os import sys import tempfile from pathlib import Path import asyncssh from asyncssh.config import SSHServerConfig try: from asyncssh.misc import IllegalUserName except Exception: # pragma: no cover IllegalUserName = asyncssh.IllegalUserName def expand_authkeys(user, cfg_text): """Load a server config exactly as SSHServerConnection does and return the expanded AuthorizedKeysFile value (a list). Raises IllegalUserName if the CVE-2026-45309 guard fires.""" with tempfile.NamedTemporaryFile("w", suffix=".conf", delete=False) as f: f.write(cfg_text) cfg_path = f.name try: # mirror connection.py:8897 # SSHServerConfig.load(last_config, config, reload, canonical, final, # accept_addr, accept_port, username, # client_host, client_addr) cfg = SSHServerConfig.load( None, cfg_path, True, False, False, "127.0.0.1", 22, user, "client.example", "203.0.113.7", ) return cfg.get("AuthorizedKeysFile") finally: os.unlink(cfg_path) def guard_blocks(user, cfg_text): """Return True if the CVE-2026-453…

Related

Alias · CVE (1)

  • CVE-2026-54590 AsyncSSH is a Python package which provides an asynchronous client and server implementation of the SSHv2 protocol on top of the Python… vulnerability

Weakness · CWE (2)

  • CWE-22 Improper Limitation of a Pathname to a Restricted Directory weakness
  • CWE-639 Authorization Bypass Through User-Controlled Key weakness

References