MediumCVE-2026-116773

    russh's SSH client is susceptible to unbounded allocations via EXT_INFO packets (CVE-2026-116773)

    Published Last modified Sources: OSV, Hersteller, CVE.org

    Share

    What is affected, what to do

    Update Eugeny Russh to version 0.65.0.

    Affected
    • Eugeny Russh lt *..0.65.0
    Fixed in
    • Eugeny Russh 0.65.0

    Not sure whether and how you are affected? Ask a question about this vulnerability

    Metrics

    4.3

    Priority

    XIEM Effective CVSS: highest available CVSS rating from NVD and vendor data.

    Source: Vendor (CNA), CVSS v3

    4.3

    CVSS v3

    Severity Medium on the scale from 0 to 10.

    Source: OSV, Hersteller, CVE.org

    0.3 %

    EPSS score

    Low — model estimates < 1% exploitation likelihood.

    Source: FIRST EPSS, percentile 17.5

    Show all metrics
    Severity
    Medium
    Priority (0–325)

    The priority score is a consolidated metric that condenses several individual signals into a single, action-guiding ranking: technical severity (CVSS), exploitation likelihood (EPSS), known-exploited status (KEV) and the context of the affected asset. The aim is to answer the only truly important question in vulnerability management: what first?

    Read in glossary
    53.95 Methodology
    Exploit maturity

    An exploit is code or a technique that specifically uses a concrete vulnerability to force unintended behaviour — such as executing code or escalating privileges. It is the tool that turns a theoretical flaw into a practical attack.

    Read in glossary
    no public PoC known
    XIEM Effective CVSS

    XIEM® is the tiered managed-security operating model from NEOSEC and a registered trademark of NEOSEC GmbH. It extends classic SIEM — the central collection, normalisation and correlation of security-relevant events — with endpoint forensics, network visibility and deception technology, and consolidates them into three building tiers: Sentry, Orchestrate and Command.

    Read in glossary
    4.3 Methodology
    CVSS v3

    CVSS is the globally established, open standard for assessing the technical severity of a vulnerability on a scale from 0 to 10. It describes how easily a flaw can be exploited and what damage it can cause — but not how likely its actual exploitation is. CVSS and the complementary EPSS deliberately answer different questions.

    Read in glossary
    4.3
    EPSS score

    The EPSS score is the absolute model output for an individual vulnerability: the estimated probability of real-world exploitation within the next 30 days, expressed as a value between 0 and 1 (i.e. 0 % to 100 %). A score of 0.42 means „the model estimates a 42 % chance of real-world exploitation within 30 days“.

    Read in glossary
    0.3 %
    Low — model estimates < 1% exploitation likelihood.
    EPSS percentile

    The EPSS percentile is the relative position of a vulnerability within all EPSS-scored CVEs, expressed as a value between 0 and 1. A percentile of 0.90 means „this CVE has a higher EPSS score than 90 % of all other scored CVEs“. Unlike the EPSS score, it is not a probability — but a positioning within a ranking.

    Read in glossary
    17.5
    Elevated — this CVE ranks above at least 10% of all CVEs scored today.
    KEV status

    The KEV catalogue is a list maintained by the US authority CISA of vulnerabilities for which active exploitation in the wild has been confirmed. It answers not the question „how severe?“ but „is it already being attacked?“ — thereby sharply prioritising patching.

    Read in glossary
    —
    Published
    CISA assessment (SSVC)
    Exploitation: proof of concept · not automatable · Technical impact: partial
    Source: CISA Vulnrichment, as of 30 October 2026
    CWE

    CWE is a catalogue of typical weakness types in software and hardware — such as flawed input validation, unsafe memory handling or poor authentication. Where a CVE names a concrete vulnerability in a concrete product, a CWE describes the underlying class of defect.

    Read in glossary
    CWE-770

    Detection rules

    No NEOSEC detection rule is available for this vulnerability yet.

    Classification under NIS2 and CRA

    Rule-based classification from exploitation status and severity. It does not replace a case-by-case assessment.

    NIS2

    NIS2 is EU Directive (EU) 2022/2555 to raise the level of cybersecurity across the Union. It substantially broadens obligations compared with the first NIS directive: more sectors, mandatory risk management, staged reporting duties and personal accountability of senior management.

    Read in glossary
    Handle the vulnerability in regular vulnerability handling under Art. 21(2)(e) NIS2.
    CRA

    The CRA is EU Regulation (EU) 2024/2847 setting horizontal cybersecurity requirements for „products with digital elements“ — from industrial controllers and software to connected consumer devices. For the first time manufacturers must ensure security across the entire product lifecycle, handle vulnerabilities and provide security updates.

    Read in glossary
    From 11.12.2027, Annex I Part II CRA requires manufacturers of affected products with digital elements to handle vulnerabilities without delay and provide security updates.
    ISO/IEC 27001

    ISO/IEC 27001 is the internationally leading standard for an information security management system (ISMS). It describes a risk-based, process-oriented framework to systematically plan, implement, review and improve information security.

    Read in glossary
    Control A.8.8, management of technical vulnerabilities: assess relevance to your systems, plan remediation and keep evidence.

    Description

    Summary

    All evidence, reproduction steps, and scripts in the report were verified against a real running instance. Nothing is fabricated or synthetic; every verdict is backed by captured output from an actual deployment. Concretely, the evidence comes from executing the real russh v0.64.1 client packet parser (Session::process_packet handling handle_ext_info) in a live client session with attacker-supplied packet bodies in the pre-authentication state; the captured output below is from that real execution.

    russh's client stores every extension name it parses out of an EXT_INFO packet into a per-session vector and never clears, caps, or deduplicates it (russh/src/client/encrypted.rs:433, field declared at russh/src/session.rs:60). The packet is accepted without limit in the pre-authentication states, so a server - which is exactly the attacker position for a client library, and remains one even after the client has verified its host key - can send an unbounded sequence of these packets and grow the client's memory for as long as the session lives: 400 packets of about 26 KB each, carrying 3000 extension names apiece, retained 49.1 MiB (about 4.8 times the wire bytes) with the vector reaching 1,200,000 entries, while an equal volume of banner packets retained nothing. Two details make this reachable in practice: russh accepts EXT_INFO in its pre-auth states with no budget of any kind (its automatic rekey read-limit only engages after authentication), and russh encodes EXT_INFO as its own private message number 7 rather than the 8308 value 82, so the check that might interoperate with a well-behaved peer is inoperative - the malicious server simply speaks number 7. The same growth pattern has been accepted by the project before for other pre-auth allocation sinks; this one is a cross-packet accumulator rather than a single-packet allocation, and none of the published advisories covers it.

    Details

    The handler appends every parsed extension name to the session state with no bound:

    russh/src/client/encrypted.rs:433 (v0.64.1 = main = e5d80a07dc554480b6ea3b2e14adfc190407b627)
        enc.received_extensions.push(name.clone());   // no cap, no dedup
    russh/src/session.rs:60
        pub received_extensions: Vec<String>,         // lives for the whole session
    

    The state machine routes EXT_INFO to this handler in both the WaitingAuthServiceRequest and WaitingAuthRequest states (encrypted.rs:111, encrypted.rs:401), i.e. before the user has authenticated, and the read-limit based automatic rekey that would otherwise throttle a flood is only allowed in the authenticated state, so no counter or budget applies pre-auth. The message number involved is russh's private constant msg::EXT_INFO = 7 (russh/src/msg.rs:32, in place since 2022), while RFC 8308 assigns EXT_INFO the value 82: russh does advertise the ext-info capability in its key exchange initiation, so a standards-compliant peer would send the packet as message number 82 - which this parser does not process - but nothing prevents a hostile or compromised server from sending message number 7 directly, and the client parses it unconditionally. Each accepted entry costs several times its wire size once heap and string overhead are counted, and the vector is only ever read (an extension lookup by the awaiter layer), never trimmed. The affected code is identical on the latest tag v0.64.1 and on main (same commit e5d80a07dc554480b6ea3b2e14adfc190407b627), and the accumulation has existed since 2022, so all released client versions are affected.

    Proof of concept

    The harness constructs a real client Session in the WaitingAuthRequest pre-auth state (the configuration a client holds while the server is asking for authentication), then feeds attacker-controlled packet bodies to the genuine Session::process_packet dispatch - the same function the network event loop calls - so the retention happens in the production parsing path, not a reimplementation. A counting global allocator measures live heap across packets. The control leg feeds the same packet volume of USERAUTH_BANNER bodies, which parse and dispatch without retention. The test below is added to the russh source tree as russh/src/client/extinfo_poc.rs, with two lines added to russh/src/client/mod.rs next to the existing test module declarations:

    #[cfg(test)]
    mod extinfo_poc;
    
    cat > russh/src/client/extinfo_poc.rs <<'EOF'
    //! PoC: unbounded pre-auth client memory growth
    //! via repeated EXT_INFO packets (`received_extensions` accumulates without
    //! cap or dedup; russh/src/client/encrypted.rs:handle_ext_info).
    //!
    //! Drives the REAL client packet parser (Session::process_packet ->
    //! handle_ext_info) with attacker-supplied packet bodies over an unmodified
    //! client session in the pre-auth state (WaitingAuthRequest). Measuring
    //! allocator totals proves the growth is persistent and unbounded across
    //! packets. Control leg: equal traffic of IGNORE packets.
    //!
    //! Run: cargo +stable-x86_64-pc-windows-gnu test -p russh --lib extinfo_poc -- --nocapture
    
    use std::alloc::{GlobalAlloc, Layout, System};
    use std::collections::HashMap;
    use std::num::Wrapping;
    use std::sync::atomic::{AtomicU64, Ordering};
    use std::sync::Arc;
    
    use tokio::sync::mpsc::{channel, unbounded_channel};
    
    use super::*;
    use crate::auth::{AuthRequest, Method};
    use crate::compression::{Compression, Decompress};
    use crate::kex::{KEXES, NONE};
    use crate::msg;
    use crate::session::{CommonSession, Encrypted, EncryptedState, Exchange};
    use crate::sshbuffer::PacketWriter;
    use crate::{cipher, CryptoVec, mac};
    
    static LIVE: AtomicU64 = AtomicU64::new(0);
    
    struct Counting;
    
    unsafe impl GlobalAlloc for Counting {
        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
            let p = System.alloc(layout);
            if !p.is_null() {
                LIVE.fetch_add(layout.size() as u64, Ordering::SeqCst);
            }
            p
        }
        unsafe fn dealloc(&self, p: *mut u8, layout: Layout) {
            LIVE.fetch_sub(layout.size() as u64, Ordering::SeqCst);
            System.dealloc(p, layout)
        }
    }
    
    #[global_allocator]
    static POC_ALLOC: Counting = Counting;
    
    fn live_mb() -> f64 {
        LIVE.load(Ordering::SeqCst) as f64 / (1024.0 * 1024.0)
    }
    
    struct PocHandler;
    
    impl Handler for PocHandler {
        type Error = crate::Error;
    
        async fn check_server_key(
            &mut self,
            _: &PublicKeyOrCertificate,
        ) -> Result<bool, Self::Error> {
            Ok(true)
        }
    }
    
    fn poc_session() -> Session {
        let config = Arc::new(Config::default());
        let (sender, receiver) = channel(config.channel_buffer_size);
        let (reply_sender, _reply_receiver) = unbounded_channel();
        let auth_request = AuthRequest::new(&Method::KeyboardInteractive {
            submethods: String::new(),
        });
        Session::new(
            config.window_size,
            CommonSession {
                auth_user: "victimuser".to_owned(),
                auth_attempts: 0,
                auth_method: Some(Method::KeyboardInteractive {
                    submethods: String::new(),
                }),
                remote_to_local: Box::new(cipher::clear::Key),
                encrypted: Some(Encrypted {
                    state: EncryptedState::WaitingAuthRequest(auth_request),
                    exchange: Some(Exchange::default()),
                    kex: KEXES.get(&NONE).unwrap().make(),
                    key: 0,
                    client_mac: mac::NONE,
                    server_mac: mac::NONE,
                    session_id: CryptoVec::new(),
                    channels: HashMap::new(),
                    last_channel_id: Wrapping(0),
                    write: Vec::new(),
                    write_cursor: 0,
                    last_rekey: russh_util::time::Instant::now(),
                    server_compression: Compression::None,
                    client_compression: Compression::None,
                    decompress: Decompress::None,
                    rekey_wanted: false,
                    received_extensions: Vec::new(),
                    extens
    

    Source: OSV (Open Source Vulnerabilities), GitHub Security Advisories, cvelistv5, NVD (NIST)

    Affected products

    The following versions are affected. Older releases of the same product line are also vulnerable unless stated otherwise.

    Affected products
    Affected versionsFixed in
    Eugenyrussh
    • < 0.65.0
    • 0.65.0
    crates.iorussh
    • —
    • 0.65
    rustrussh
    • < 0.65
    • 0.65

    Vendors and products

    Weakness classes (CWE)

    • CWE-770Allocation of Resources Without Limits or Throttling

      Base

      The product allocates a reusable resource or group of resources on behalf of an actor without imposing any intended restrictions on the size or number of resources that can be allocated.

      Entry at MITRE CWE-770

    References & sources

    • github.comEugeny/russh/commit/b33e93beec393e6d29eee69c1b8831736724ee75x_refsource_MISC, web
    • github.comEugeny/russh/releases/tag/v0.65.0x_refsource_MISC, web
    • github.comEugeny/russh/security/advisories/GHSA-4p9f-7p9m-5vfgx_refsource_CONFIRM, web
    • NVD (NIST)vuln/detail/CVE-2026-116773advisory
    • github.comEugeny/russhpackage

    Changes to this CVE

    Who changed this CVE and when: NVD (analysis, affected products, CVSS), the CVE publisher (CNA) and additional providers such as CISA (ADP).

    1. Record modifiedCISA ADP

      CISA assessment (SSVC): exploitation: proof of concept, automatable: no

      Show 2 original changes
      Individual changes of this event as published by NVD
      TypeActionBeforeAfter
      Referenceadded
      —
      https://github.com/Eugeny/russh/security/advisories/GHSA-4p9f-7p9m-5vfg
      SSVC assessmentadded
      —
      {"id":"CVE-2026-116773","role":"CISA Coordinator","options":[{"exploitation":"poc"},{"automatable":"no"},{"technicalImpact":"partial"}],"version":"2.0.3","timestamp":"2026-10-30T15:17:39.066575Z"}

      Source identifier: 134c704f-9b21-4f2e-91b3-4a467353bcc0

    2. ReceivedGitHub

      Show 7 original changes
      Individual changes of this event as published by NVD
      TypeActionBeforeAfter
      Descriptionadded
      —
      russh is a Rust SSH client and server library that includes an SSH agent protocol implementation. Prior to 0.65.0, the client Session::process_packet path in russh/src/client/encrypted.rs accepts repeated packets encoded with russh's private msg::EXT_INFO = 7 before authentication, while the standards-compliant EXT_INFO message number 82 does not reach the affected handler. The handle_ext_info function appends every parsed extension name to received_extensions without clearing, deduplicating, or bounding the vector. A malicious SSH server that deliberately sends private message number 7 can therefore retain an unbounded number of attacker-controlled extension names for the session lifetime, causing progressive client memory exhaustion. This issue is fixed in version 0.65.0.
      CVSSadded
      —
      AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L
      Weakness type (CWE)added
      —
      CWE-770
      Affected versions (publisher)added
      —
      New affected value received. <a href="https://github.com/CVEProject/cvelistV5/blob/main/cves/2026/116xxx/CVE-2026-116773.json">CVE-2026-116773</a>
      Referenceadded
      —
      https://github.com/Eugeny/russh/commit/b33e93beec393e6d29eee69c1b8831736724ee75
      Referenceadded
      —
      https://github.com/Eugeny/russh/releases/tag/v0.65.0
      Referenceadded
      —
      https://github.com/Eugeny/russh/security/advisories/GHSA-4p9f-7p9m-5vfg

      Source identifier: security-advisories@github.com

    Embed badge

    Show the severity and KEV status of this vulnerability on your site – with a link back here. The image is updated hourly.

    Badge preview for CVE-2026-116773
    <a href="https://intel.neosec.eu/en/cve/CVE-2026-116773"><img src="https://intel.neosec.eu/badge/cve/CVE-2026-116773.svg?lang=en" alt="CVE-2026-116773 – NEOSEC Intel" height="20"></a>