CVE-2026-54590
AsyncSSH AuthorizedKeysFile username substitution bypass through ~ and environment expansion
AsyncSSH is a Python package which provides an asynchronous client and server implementation of the SSHv2 protocol on top of the Python asyncio framework. Version 2.23.0 contains an incomplete fix for CVE-2026-45309 in SSHServerConfig._set_tokens that blocks /, , and .. before %u substitution in AuthorizedKeysFile but does not block a leading ~ or ${ENV}, allowing later expansion in _expand_val and Path(filename).expanduser() to escape the intended authorized-keys directory. This issue is fixed in version 2.23.1.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- High
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- High
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
5.9
AsyncSSH AuthorizedKeysFile username substitution bypass through ~ and environment expansion
AsyncSSH is a Python package which provides an asynchronous client and server implementation of the SSHv2 protocol on top of the Python asyncio framework. Version 2.23.0 contains an incomplete fix for CVE-2026-45309 in SSHServerConfig._set_tokens that blocks /, , and .. before %u substitution in AuthorizedKeysFile but does not block a leading ~ or ${ENV}, allowing later expansion in _expand_val and Path(filename).expanduser() to escape the intended authorized-keys directory. This issue is fixed in version 2.23.1.
- Record
CVE-2026-54590- CVSS 3.1
- 5.9 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N - Weakness
- CWE-22, CWE-639
- Updated
- 2026-11-02
EUVDEUVD-2026-42400
5.9
AsyncSSH is a Python package which provides an asynchronous client and server implementation of the SSHv2 protocol on top of the Python asyncio framework. Version 2.23.0 contains an incomplete fix for CVE-2026-45309 in SSHServerConfig._set_tokens that blocks /, , and .. before %u substitution in AuthorizedKeysFile but does not block a leading ~ or ${ENV}, allowing later expansion in _expand_val and Path(filename).expanduser() to escape the intended authorized-keys directory. This issue is fixed in version 2.23.1.
- Record
EUVD-2026-42400- CVSS 3.1
- 5.9 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-qr67-gv47-xwwh
5.9
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
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):
%umust be the leading path component; on Python 3.13,Path('~nonexistentuser').expanduser()raisesRuntimeError, 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…
- Record
GHSA-qr67-gv47-xwwh- CVSS 3.1
- 5.9 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N - Weakness
- CWE-22, CWE-639
- Updated
- 2026-11-02
NVD
5.9
AsyncSSH is a Python package which provides an asynchronous client and server implementation of the SSHv2 protocol on top of the Python asyncio framework. Version 2.23.0 contains an incomplete fix for CVE-2026-45309 in SSHServerConfig._set_tokens that blocks /, , and .. before %u substitution in AuthorizedKeysFile but does not block a leading ~ or ${ENV}, allowing later expansion in _expand_val and Path(filename).expanduser() to escape the intended authorized-keys directory. This issue is fixed in version 2.23.1.
- Record
CVE-2026-54590- CVSS 3.1
- 5.9 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N - Weakness
- CWE-22, CWE-639
- Updated
- 2026-11-02
OSV
5.9
AsyncSSH AuthorizedKeysFile username substitution bypass through ~ and environment expansion
AsyncSSH is a Python package which provides an asynchronous client and server implementation of the SSHv2 protocol on top of the Python asyncio framework. Version 2.23.0 contains an incomplete fix for CVE-2026-45309 in SSHServerConfig._set_tokens that blocks /, , and .. before %u substitution in AuthorizedKeysFile but does not block a leading ~ or ${ENV}, allowing later expansion in _expand_val and Path(filename).expanduser() to escape the intended authorized-keys directory. This issue is fixed in version 2.23.1.
- Record
CVE-2026-54590- CVSS 3.1
- 5.9 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N - Weakness
- CWE-22, CWE-639
- Updated
- 2026-11-02
OSVDEBIAN-CVE-2026-54590
5.9
AsyncSSH is a Python package which provides an asynchronous client and server implementation of the SSHv2 protocol on top of the Python asyncio framework. Version 2.23.0 contains an incomplete fix for CVE-2026-45309 in SSHServerConfig._set_tokens that blocks /, , and .. before %u substitution in AuthorizedKeysFile but does not block a leading ~ or ${ENV}, allowing later expansion in _expand_val and Path(filename).expanduser() to escape the intended authorized-keys directory. This issue is fixed in version 2.23.1.
- Record
DEBIAN-CVE-2026-54590- CVSS 3.1
- 5.9 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N - Updated
- 2026-11-02
OSVGHSA-qr67-gv47-xwwh
5.9
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
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):
%umust be the leading path component; on Python 3.13,Path('~nonexistentuser').expanduser()raisesRuntimeError, 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…
- Record
GHSA-qr67-gv47-xwwh- CVSS 3.1
- 5.9 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N - Weakness
- CWE-22, CWE-639
- Updated
- 2026-11-02
OSVPYSEC-2026-3809
5.9
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
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):
%umust be the leading path component; on Python 3.13,Path('~nonexistentuser').expanduser()raisesRuntimeError, 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…
- Record
PYSEC-2026-3809- CVSS 3.1
- 5.9 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N - Updated
- 2026-11-02
OSVUBUNTU-CVE-2026-54590
5.9
AsyncSSH is a Python package which provides an asynchronous client and server implementation of the SSHv2 protocol on top of the Python asyncio framework. Version 2.23.0 contains an incomplete fix for CVE-2026-45309 in SSHServerConfig._set_tokens that blocks /, , and .. before %u substitution in AuthorizedKeysFile but does not block a leading ~ or ${ENV}, allowing later expansion in _expand_val and Path(filename).expanduser() to escape the intended authorized-keys directory. This issue is fixed in version 2.23.1.
- Record
UBUNTU-CVE-2026-54590- CVSS 3.1
- 5.9 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
asyncsshPyPI |
through 2.23.0 | 2.23.1 |
python-asyncsshDebian:14 |
before 2.24.0-1, plus listed versions 2.20.0-12.21.0-12.21.1-12.22.0-12.22.0-22.23.0-1 |
2.24.0-1 |
asyncsshPyPI |
before 2.23.1, plus listed versions 0.8.10.8.20.8.30.8.40.9.0 Show 45 moreShow less0.9.10.9.21.0.01.0.11.1.01.1.11.10.01.10.11.11.01.11.11.12.01.12.11.12.21.13.01.13.11.13.21.13.31.14.01.15.01.15.11.16.01.16.11.17.01.17.11.18.01.2.01.2.11.3.01.3.11.3.21.4.01.4.11.5.01.5.11.5.21.5.31.5.41.5.51.5.61.6.01.6.11.6.21.7.11.7.21.7.3 |
2.23.1 |
python-asyncsshUbuntu:16.04:LTS |
All versions, plus listed versions 1.2.0-11.2.1-11.3.0-1 |
None known |
python-asyncsshUbuntu:Pro:18.04:LTS |
All versions, plus listed versions 1.8.1-21.11.0-21.11.1-11.11.1-1ubuntu0.1~esm11.11.1-1ubuntu0.1~esm2 |
None known |
python-asyncsshUbuntu:20.04:LTS |
All versions, plus listed versions 1.12.2-11.12.2-1ubuntu0.11.12.2-1ubuntu0.2 |
None known |
python-asyncsshUbuntu:Pro:22.04:LTS |
All versions, plus listed versions 2.5.0-12.5.0-1ubuntu0.1~esm12.5.0-1ubuntu0.12.5.0-1ubuntu0.1+esm1 |
None known |
python-asyncsshUbuntu:Pro:24.04:LTS |
All versions, plus listed versions 2.10.1-22.10.1-2ubuntu0.12.10.1-2ubuntu0.1+esm1 |
None known |
python-asyncsshUbuntu:25.10 |
All versions, plus listed versions 2.20.0-1 |
None known |
python-asyncsshUbuntu:26.10 |
All versions, plus listed versions 2.21.1-12.23.0-1 |
None known |
python-asyncsshUbuntu:26.04:LTS |
All versions, plus listed versions 2.20.0-12.21.1-1 |
None known |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-54590 is a medium vulnerability (CVSS 5.9) in Ronf Asyncssh, published July 8, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.34%, the 27th percentile. Fixed in asyncssh 2.23.1.
References
github.com/ronf/asyncssh/security/advisories/GHSA-qr67-gv47-xwwh
github.com/ronf/asyncssh/commit/3d515ba9ba0cd9990d248bdf62bcf05d51261a88
github.com/ronf/asyncssh/releases/tag/v2.23.1
nvd.nist.gov/vuln/detail/CVE-2026-54590
github.com/advisories/GHSA-qr67-gv47-xwwh
github.com/CVEProject/cvelistV5/tree/main/cves/2026/54xxx/CVE-2026-54590.json
Left out here: 40 versions and the rest of 3 source descriptions. The JSON record has them.