PT-2026-67476 · Crates.Io · Russh
Published
2026-07-24
·
Updated
2026-07-24
CVSS v3.1
5.3
Medium
| Vector | AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L |
Summary
A malicious SSH server can crash a
russh client session with a single
malformed key-exchange reply, causing a pre-authentication Denial-of-Service
before the server host key is verified. The embedding process itself stays
up, but the connection is killed deterministically.Details
Every other kex path in
russh validates the peer ephemeral length before
cloning:Curve25519Kex::server dh(russh/src/kex/curve25519.rs:61-65) checksif pubkey len != 32 { return Err(crate::Error::Kex); }beforeclone from slice.- The hybrid ML-KEM, ECDH-NIST, and DH/GEX paths all validate lengths.
Only the client-side curve25519
compute shared secret is missing the check.
This asymmetric validation gap makes the bug easy to miss in code review: a
malicious client cannot panic a russh server this way (the server path
checks the length), but a malicious server can panic a russh client.Incriminated source code (repo-relative paths):
- Vulnerable
compute shared secret:russh/src/kex/curve25519.rs:110-117(panic at line 113) - Client-side entry point:
russh/src/client/kex.rs:266-277(KEX ECDH REPLY→Bytes::decode→compute shared secret) - Server-side contrast (has the length check):
russh/src/kex/curve25519.rs:51-88(server dh) - Session spawn site:
russh/src/client/mod.rs(connect stream→russh util::runtime::spawn) - Runtime wrapper:
russh-util/src/runtime.rs:37-48(spawnwrapstokio::spawn; panic surfaces asJoinError)
PoC
A standalone, self-contained Cargo PoC is provided in
vuln poc/vuln 002 client wronglen x25519 panic/ in this repo. It installs a
global panic hook that sets an AtomicBool if any panic fires, starts a
malicious raw SSH server on 127.0.0.1:0 that completes the SSH id and
KEXINIT exchange, reads the client KEX ECDH INIT, and sends
KEX ECDH REPLY with a 16-byte server ephemeral (instead of 32) and a fake
signature. It then calls russh::client::connect with Preferred::kex set
to curve25519-sha256 and a handler that accepts any server key (the check
is never reached because the client panics first) and prints a clear verdict.Build & run:
bash
cd vuln poc/vuln 002 client wronglen x25519 panic
cargo run --releaseExpected output (verdict line, from a successful reproduction):
[poc] panic captured: panicked at russh/src/kex/curve25519.rs:113:25:
copy from slice: source slice length (16) does not match destination slice length (32)
[!] Vulnerability reproduced: russh client panicked in Curve25519Kex::compute shared secret
on a wrong-length (16-byte) server ephemeral before verifying the host key signature
(pre-auth client DoS).The malicious payload is the
f field of KEX ECDH REPLY:MSG KEX ECDH REPLY (1 byte, value 0x1f)
string K S (server host key blob — any valid-looking bytes)
string f (server ephemeral — 16 bytes of 0x00 instead of 32)
string signature (fake; never verified by the client)The length prefix of
f is 4 (u32 BE) = 16, followed by 16 bytes. The
russh client decodes this into exchange.server ephemeral (a Vec<u8> of
length 16) and passes it to compute shared secret, which panics on
clone from slice.Impact
What kind of vulnerability: CWE-704 (incorrect type conversion / cast —
clone from slice length mismatch) → deterministic panic → pre-authentication
per-connection Denial-of-Service. The attacker does not need the server's
private key; any network position that can deliver a malformed
KEX ECDH REPLY (a rogue server, or a MitM before authentication) suffices.Who is impacted: any deployment that uses
russh::client::connect (or
connect stream) to connect to an attacker-controlled or MitM-reachable SSH
server, and that negotiates curve25519-sha256 (the default and
most-preferred kex algorithm in russh). A single malformed
KEX ECDH REPLY kills the client session; the attack is deterministic and
single-packet. The panic is isolated to the spawned session task
(tokio::spawn catches it and surfaces a JoinError), so the embedding
process keeps running — the impact is per-connection DoS, not process crash,
unless the embedder installs a custom panic hook that calls
std::process::abort.Workaround: until a fix is released, clients can reduce exposure by
disabling
curve25519-sha256 in the Preferred::kex list and preferring a
kex algorithm whose peer-ephemeral length is validated (e.g. the ECDH-NIST
or DH/GEX paths). This is a mitigation, not a fix.Suggested fix (one-line length check, mirrors the existing server-side
server dh check):rust
// russh/src/kex/curve25519.rs, at the top of compute shared secret:
fn compute shared secret(&mut self, remote pubkey : &[u8]) -> Result<(), crate::Error> {
if remote pubkey .len() != 32 {
return Err(crate::Error::Kex);
}
let local secret = self.local secret.take().ok or(crate::Error::KexInit)?;
let mut remote pubkey = MontgomeryPoint([0; 32]);
remote pubkey.0.clone from slice(remote pubkey );
let shared = local secret * remote pubkey;
self.shared secret = Some(shared);
Ok(())
}This makes the client-side
compute shared secret consistent with the
existing server-side server dh check at russh/src/kex/curve25519.rs:61-65
and with the other kex paths that already validate peer ephemeral lengths.[vuln poc.zip](https://github.com/user-attachments/files/29255207/vuln poc.zip)
Fix
Improper Check for Exceptional Conditions
Incorrect Type Conversion or Cast
Found an issue in the description? Have something to add? Feel free to write us 👾
Related Identifiers
Affected Products
Russh