PT-2026-67476 · Crates.Io · Russh

Published

2026-07-24

·

Updated

2026-07-24

CVSS v3.1

5.3

Medium

VectorAV: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) checks if pubkey len != 32 { return Err(crate::Error::Kex); } before clone 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 REPLYBytes::decodecompute 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 streamrussh util::runtime::spawn)
  • Runtime wrapper: russh-util/src/runtime.rs:37-48 (spawn wraps tokio::spawn; panic surfaces as JoinError)

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 --release
Expected 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.

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 👾

Weakness Enumeration

Related Identifiers

GHSA-G9HV-X236-4QP3

Affected Products

Russh