A AegiFlow
MEDIUMCVSS 5.9EPSS 0.4%

CVE-2026-54590

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

Published
2026-08-26
Modified
2026-08-26
EPSS percentile
32%
Aliases
GHSA-qr67-gv47-xwwh
Sources
github-advisory

Summary

**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 `ConfigP

Affected packages

EcosystemPackageAffected versionsFixed versions
PyPIasyncssh2.23.1

Remediation: Upgrade to 2.23.1 or later.

References

Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.

CVE® is a registered trademark of The MITRE Corporation. CVE content reproduced under the CVE Terms of Use; copyright designation © MITRE.

EPSS scores provided by the FIRST.org Exploit Prediction Scoring System.