A AegiFlow
LOWCVSS 2.3

CVE-2026-111062

A russh SSH server accepts a publickey authentication whose signature uses a different algorithm than the one named in the request

Published
2026-10-19
Modified
2026-10-19
Aliases
GHSA-j7h2-66j4-ghjf
Sources
github-advisory

Summary

### 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`. ```rust // ---- 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 { Ok(Auth::Accept) } async fn auth_publickey( &mut self, _user: &str, _public_key: &PublicKey, ) -> Result { 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 , announced: &str, hash: Option , ) -> Vec { 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 priva

Affected packages

EcosystemPackageAffected versionsFixed versions
rustrussh—0.64.0

Remediation: Upgrade to 0.64.0 or later.

References

Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.

CVE® is a registered trademark of The MITRE Corporation. CVE content reproduced under the CVE Terms of Use; copyright designation © MITRE.