Not ratedCVE-2026-111062

    Russh: Improper Verification of Cryptographic Signature (CVE-2026-111062)

    Published Sources: OSV, Hersteller

    What is affected, what to do

    Update Russh to version 0.64.0.

    Affected
    • Russh *..>= 0.34.0-beta.1, < 0.64.0
    • Russh 0.34.0-beta.1..*
    Fixed in
    • Russh 0.64.0

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

    Metrics

    —

    Priority

    No value is available for this vulnerability.

    —

    CVSS v3

    No value is available for this vulnerability.

    —

    EPSS score

    No value is available for this vulnerability.

    Show all metrics
    Severity
    Not rated
    Priority score

    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
    30.00
    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
    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
    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-347

    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

    Details

    In russh/src/server/encrypted.rs, server_read_auth_request_pk (0.63.3, around lines 880-990):

    • The requested algorithm name (pubkey_algo) is read and only used to decode the key: PublicKeyOrCertificate::decode(&pubkey_algo, &pubkey_key). The derived hash_alg is then dropped (PublicKeyOrCertificate::PublicKey { ref key, .. }).
    • The signature blob is decoded (Signature::decode) and verified with Verifier::verify(&pubkey, &buf, &sig). ssh-key picks the hash from the signature's own algorithm (sig.algorithm()).
    • pubkey_algo and sig.algorithm() are never compared.

    So for an RSA key, the request can say rsa-sha2-512 while the signature is ssh-rsa. The signature is valid for SHA-1, verification succeeds, and Handler::auth_publickey is called.

    Affected versions: every russh release, from the first (0.34.0-beta.1, 2022-03) through 0.63.3 and current main. The behaviour is inherited from thrussh, which russh was forked from.

    The mechanism changed over time, but the outcome is the same: the hash is always taken from the signature blob, never checked against the request.

    • Up to 0.46: after reading the algorithm name inside the signature blob, russh overwrites the key's hash with it (pubkey.set_algorithm(algo_)), then verifies.
    • From 0.47/0.48 (move to ssh-key): the signature is decoded with its own algorithm (sig_workaround::Sig::decode, later Signature::decode) and verified with it.

    Confirmed by running the PoC below on 0.63.3. Confirmed by reading the code of server/encrypted.rs in 0.34.0-beta.1, 0.34.0, 0.36.0, 0.36.1, 0.37.1, 0.40.2, 0.43.0, 0.45.0, 0.46.0, 0.48.2, 0.50.4, 0.54.6, 0.58.1, 0.63.1, 0.63.3 and main (2026-09-30), and in thrussh 0.33.5.

    PoC

    A unit test in the existing test module of russh/src/server/encrypted.rs. It uses the module's own helpers (test_auth_session, sign_with_hash_alg, PrivateKeyWithHashAlg) and needs no network or configuration.

    1. Check out russh 0.63.3, or main.
    2. Paste the code below inside mod tests { ... } of russh/src/server/encrypted.rs.
    3. Run cargo test --lib poc_sha1.
    // ---- PoC: signature algorithm not checked against the requested one ----
    
    /// Records whether `auth_publickey` (called only after the signature
    /// verified) was reached.
    #[derive(Default)]
    struct AcceptAnyKey {
        authenticated: bool,
    }
    
    impl Handler for AcceptAnyKey {
        type Error = Error;
    
        async fn auth_publickey_offered(
            &mut self,
            _user: &str,
            _public_key: &PublicKey,
        ) -> Result<Auth, Self::Error> {
            Ok(Auth::Accept)
        }
    
        async fn auth_publickey(
            &mut self,
            _user: &str,
            _public_key: &PublicKey,
        ) -> Result<Auth, Self::Error> {
            self.authenticated = true;
            Ok(Auth::Accept)
        }
    }
    
    /// A signed publickey request that names `announced` as its public key
    /// algorithm, but is signed with `hash` (`None` = `ssh-rsa`, SHA-1).
    fn signed_request_announcing(
        user: &str,
        private_key: Arc<PrivateKey>,
        announced: &str,
        hash: Option<crate::keys::HashAlg>,
    ) -> Vec<u8> {
        let mut packet = Vec::new();
        packet.push(msg::USERAUTH_REQUEST);
        user.encode(&mut packet).unwrap();
        "ssh-connection".encode(&mut packet).unwrap();
        "publickey".encode(&mut packet).unwrap();
        1u8.encode(&mut packet).unwrap();
        announced.encode(&mut packet).unwrap();
        private_key.public_key().to_bytes().unwrap().encode(&mut packet).unwrap();
    
        let mut signed = Vec::new();
        CryptoVec::new().as_ref().encode(&mut signed).unwrap();
        signed.extend_from_slice(&packet);
        let signature =
            sign_with_hash_alg(&PrivateKeyWithHashAlg::new(private_key, hash), &signed).unwrap();
        signature.encode(&mut packet).unwrap();
        packet
    }
    
    #[tokio::test]
    async fn poc_sha1_signature_accepted_when_rsa_sha2_512_is_announced() {
        let rsa = ssh_key::private::RsaKeypair::random(&mut rand::rng(), 2048).unwrap();
        let private = Arc::new(PrivateKey::from(rsa));
        let mut session = test_auth_session();
        let mut handler = AcceptAnyKey::default();
    
        // The request says rsa-sha2-512; the signature blob is ssh-rsa (SHA-1).
        let packet = signed_request_announcing("alice", private, "rsa-sha2-512", None);
        session.process_packet(&mut handler, &packet).await.unwrap();
    
        // RFC 8332 §3: the signature algorithm must be the requested one, so
        // this request must be rejected. On russh 0.63.3 it is accepted.
        assert!(
            !handler.authenticated,
            "an ssh-rsa (SHA-1) signature was accepted for a request announcing rsa-sha2-512"
        );
    }
    

    Result on unmodified 0.63.3:

    test server::encrypted::tests::poc_sha1_signature_accepted_when_rsa_sha2_512_is_announced ... FAILED
    panicked at src/server/encrypted.rs: an ssh-rsa (SHA-1) signature was accepted for a request announcing rsa-sha2-512
    

    With the fix below, the test passes and the whole library suite still passes: cargo test --lib gives 186 passed.

    Suggested fix: reject a mismatch through the existing reject_auth_request(until, ...) path, so the constant rejection time is kept.

    --- a/russh/src/server/encrypted.rs
    +++ b/russh/src/server/encrypted.rs
    @@ -1023,6 +1023,21 @@
                         map_err!(ensure_end(&signature_reader))?;
                         map_err!(ensure_end(r))?;
     
    +                    // RFC 8332 §3: the signature must use the algorithm the
    +                    // request names.
    +                    let offered_algorithm = match pk_or_cert {
    +                        PublicKeyOrCertificate::PublicKey { .. } => ssh_key::Algorithm::new(&pubkey_algo),
    +                        PublicKeyOrCertificate::Certificate(_) => {
    +                            <ssh_key::Algorithm as crate::helpers::AlgorithmExt>::new_certificate_ext(&pubkey_algo)
    +                        }
    +                    };
    +                    if offered_algorithm.ok().as_ref() != Some(&sig.algorithm()) {
    +                        debug!("signature algorithm does not match the requested one");
    +                        auth_user.clear();
    +                        reject_auth_request(until, &mut self.write, auth_request).await?;
    +                        return Ok(());
    +                    }
    +
                         let is_valid = if accepted_probe_matches && user == auth_user {
                             true
                         } else {
    

    Impact

    This is a missing check on the signature algorithm (improper verification of a cryptographic signature's algorithm, CWE-347) in the server's publickey authentication.

    Who is impacted: applications that use russh as an SSH server and rely on the requested algorithm to restrict, record or audit user-key signature algorithms. For example:

    • a server that refuses SHA-1 (ssh-rsa) signatures, as OpenSSH does by default since 8.8;
    • a server that logs which algorithm a client authenticated with.

    Such a server can be made to accept a SHA-1 signature, or to record rsa-sha2-512 for one, by any holder of a valid RSA key.

    Not impacted:

    • russh clients;
    • authentication strength beyond the choice of hash, since a valid signature by the private key is still required;
    • Te RSA keys were examined, the case where one key admits several signature algorithms (ssh-rsa, rsa-sha2-256, rsa-sha2-512). We did not test other key types.

    Source: OSV (Open Source Vulnerabilities), GitHub Security Advisories

    Affected products

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

    Affected products
    VendorProductAffected versionsFixed in
    crates.iorussh
    • 0.34.0-beta.1
    • 0.64.0
    rustrussh
    • >= 0.34.0-beta.1, < 0.64.0
    • 0.64.0

    Weakness classes (CWE)

    • CWE-347Improper Verification of Cryptographic Signature

      Base

      The product does not verify, or incorrectly verifies, the cryptographic signature for data.

      Entry at MITRE CWE-347

    References & sources

    • github.comEugeny/russh/security/advisories/GHSA-j7h2-66j4-ghjfweb
    • github.comEugeny/russh/commit/a0ded76bdc352f8fbeaaeb175fb2a0c586fbc8f2web
    • github.comEugeny/russhpackage
    • github.comEugeny/russh/releases/tag/v0.64.0web