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
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
References
- https://github.com/ronf/asyncssh/security/advisories/GHSA-qr67-gv47-xwwh web
- https://nvd.nist.gov/vuln/detail/CVE-2026-54590 advisory
- https://github.com/ronf/asyncssh/commit/3d515ba9ba0cd9990d248bdf62bcf05d51261a88 web
- https://github.com/ronf/asyncssh package
- https://github.com/ronf/asyncssh/releases/tag/v2.23.1 web