VYPR
Medium severity5.3NVD Advisory· Published Jun 10, 2026

CVE-2026-48108

CVE-2026-48108

Description

Russh SSH library versions before 0.61.0 allowed remote peers to hold connection setup resources with malformed identification input.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Russh SSH library versions before 0.61.0 allowed remote peers to hold connection setup resources with malformed identification input.

Vulnerability

Russh, an SSH client and server library, versions 0.34.0-beta.1 through 0.60.0, did not strictly enforce SSH identification string rules. The server-side identification reader incorrectly allowed pre-banner lines from clients and did not limit the number of such lines, deviating from RFC 4253 [1]. This vulnerability exists in the russh/src/ssh_read.rs and russh/src/server/mod.rs files.

Exploitation

An attacker can exploit this by sending malformed identification input to a server built with a vulnerable version of russh. The server will accept multiple non-SSH lines as pre-banner text instead of rejecting them, keeping connection setup resources occupied. This process can be repeated until an application-level timeout or external resource limit is reached [1].

Impact

Successful exploitation allows a remote peer to hold connection setup resources in the cleartext pre-authentication phase. While this does not disclose application secrets, credentials, keys, or authenticated user data, it can be used to probe server-side parser acceptance behavior, potentially making server fingerprinting easier [1].

Mitigation

This issue has been patched in russh version 0.61.0. Users are advised to upgrade to version 0.61.0 or later. No workarounds are available for earlier versions [1].

AI Insight generated on Jun 10, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Eugeny/Russhinferred2 versions
    >=0.34.0-beta.1,<0.61.0+ 1 more
    • (no CPE)range: >=0.34.0-beta.1,<0.61.0
    • (no CPE)range: >=0.34.0-beta.1 <0.61.0

Patches

2
b0961adf47d6

parser: reject trailing KEX and channel-open payloads

https://github.com/Eugeny/russhMika CohenMay 16, 2026Fixed in 0.61.0via llm-release-walk
5 files changed · +587 8
  • russh/src/client/kex.rs+8 1 modified
    @@ -14,6 +14,7 @@ use crate::kex::dh::groups::DhGroup;
     use crate::kex::{KEXES, KexAlgorithm, KexAlgorithmImplementor, KexCause, KexProgress};
     use crate::keys::key::parse_public_key;
     use crate::negotiation::{Names, Select};
    +use crate::parsing::ensure_end;
     use crate::session::Exchange;
     use crate::sshbuffer::PacketWriter;
     use crate::{CryptoVec, Error, SshId, msg, negotiation, strict_kex_violation};
    @@ -195,6 +196,7 @@ impl ClientKex {
     
                     let prime = Mpint::decode(&mut r)?;
                     let generator = Mpint::decode(&mut r)?;
    +                ensure_end(&r)?;
                     debug!("received gex group: prime={prime}, generator={generator}");
     
                     let group = DhGroup {
    @@ -287,7 +289,10 @@ impl ClientKex {
                     })?;
     
                     let signature = Bytes::decode(r)?;
    -                let signature = Signature::decode(&mut &signature[..])?;
    +                let mut signature_reader = &signature[..];
    +                let signature = Signature::decode(&mut signature_reader)?;
    +                ensure_end(&signature_reader)?;
    +                ensure_end(r)?;
     
                     if let Err(e) =
                         signature::Verifier::verify(&server_host_key, hash.as_ref(), &signature)
    @@ -338,6 +343,8 @@ impl ClientKex {
                         );
                         return Err(Error::Kex);
                     }
    +                let r = &input.buffer[1..];
    +                ensure_end(&r)?;
     
                     Ok(KexProgress::Done {
                         newkeys,
    
  • russh/src/negotiation.rs+3 0 modified
    @@ -26,6 +26,7 @@ use crate::kex::{
         EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT, EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER, KexCause,
     };
     use crate::keys::key::safe_rng;
    +use crate::parsing::ensure_end;
     #[cfg(not(target_arch = "wasm32"))]
     use crate::server::Config;
     use crate::sshbuffer::PacketWriter;
    @@ -343,6 +344,8 @@ pub(crate) trait Select {
             String::decode(&mut r)?; // languages server-to-client
     
             let follows = u8::decode(&mut r)? != 0;
    +        u32::decode(&mut r)?;
    +        ensure_end(&r)?;
             Ok(Names {
                 kex: kex_algorithm,
                 key: key_algorithm,
    
  • russh/src/parsing.rs+99 6 modified
    @@ -4,6 +4,23 @@ use crate::msg;
     
     use crate::map_err;
     
    +/// Require a decoded known-message payload to be fully consumed.
    +///
    +/// SSH RFCs and implemented OpenSSH extensions define exact field layouts for
    +/// known message types. Callers use this after decoding those fields so
    +/// malformed packets with trailing payload bytes are rejected instead of being
    +/// treated as canonical messages.
    +pub(crate) fn ensure_end(reader: &impl Reader) -> Result<(), crate::Error> {
    +    if reader.is_finished() {
    +        Ok(())
    +    } else {
    +        Err(ssh_encoding::Error::TrailingData {
    +            remaining: reader.remaining_len(),
    +        }
    +        .into())
    +    }
    +}
    +
     #[derive(Debug)]
     pub struct OpenChannelMessage {
         pub typ: ChannelType,
    @@ -13,6 +30,12 @@ pub struct OpenChannelMessage {
     }
     
     impl OpenChannelMessage {
    +    /// Parse an SSH `CHANNEL_OPEN` payload.
    +    ///
    +    /// Known channel types are parsed according to their fixed layouts and must
    +    /// not contain trailing bytes. Unknown extension channel types remain
    +    /// intentionally opaque so applications can implement extension-specific
    +    /// parsing and compatibility behavior.
         pub fn parse<R: Reader>(r: &mut R) -> Result<Self, crate::Error> {
             // https://tools.ietf.org/html/rfc4254#section-5.1
             let typ = map_err!(String::decode(r))?;
    @@ -21,24 +44,46 @@ impl OpenChannelMessage {
             let maxpacket = map_err!(u32::decode(r))?;
     
             let typ = match typ.as_str() {
    -            "session" => ChannelType::Session,
    +            "session" => {
    +                ensure_end(r)?;
    +                ChannelType::Session
    +            }
                 "x11" => {
                     let originator_address = map_err!(String::decode(r))?;
                     let originator_port = map_err!(u32::decode(r))?;
    +                ensure_end(r)?;
                     ChannelType::X11 {
                         originator_address,
                         originator_port,
                     }
                 }
    -            "direct-tcpip" => ChannelType::DirectTcpip(TcpChannelInfo::decode(r)?),
    +            "direct-tcpip" => {
    +                let info = TcpChannelInfo::decode(r)?;
    +                ensure_end(r)?;
    +                ChannelType::DirectTcpip(info)
    +            }
                 "direct-streamlocal@openssh.com" => {
    -                ChannelType::DirectStreamLocal(StreamLocalChannelInfo::decode(r)?)
    +                let info = StreamLocalChannelInfo::decode(r)?;
    +                String::decode(r)?; // originator address/reserved
    +                u32::decode(r)?; // originator port/reserved
    +                ensure_end(r)?;
    +                ChannelType::DirectStreamLocal(info)
    +            }
    +            "forwarded-tcpip" => {
    +                let info = TcpChannelInfo::decode(r)?;
    +                ensure_end(r)?;
    +                ChannelType::ForwardedTcpIp(info)
                 }
    -            "forwarded-tcpip" => ChannelType::ForwardedTcpIp(TcpChannelInfo::decode(r)?),
                 "forwarded-streamlocal@openssh.com" => {
    -                ChannelType::ForwardedStreamLocal(StreamLocalChannelInfo::decode(r)?)
    +                let info = StreamLocalChannelInfo::decode(r)?;
    +                String::decode(r)?; // reserved
    +                ensure_end(r)?;
    +                ChannelType::ForwardedStreamLocal(info)
    +            }
    +            "auth-agent@openssh.com" => {
    +                ensure_end(r)?;
    +                ChannelType::AgentForward
                 }
    -            "auth-agent@openssh.com" => ChannelType::AgentForward,
                 _ => ChannelType::Unknown { typ },
             };
     
    @@ -177,3 +222,51 @@ impl Decode for ChannelOpenConfirmation {
             })
         }
     }
    +
    +#[cfg(test)]
    +mod tests {
    +    use super::{ChannelType, OpenChannelMessage};
    +    use crate::tests::raw_no_crypto::{channel_open_payload, encode_string, push_u32};
    +
    +    #[test]
    +    fn known_channel_open_with_trailing_bytes_is_rejected() {
    +        let mut payload = channel_open_payload(b"session");
    +        payload.push(0);
    +
    +        assert!(
    +            OpenChannelMessage::parse(&mut payload.as_slice()).is_err(),
    +            "known channel-open type accepted trailing bytes"
    +        );
    +    }
    +
    +    #[test]
    +    fn unknown_channel_open_with_extra_payload_stays_permissive() {
    +        let mut payload = channel_open_payload(b"unknown@example.com");
    +        payload.extend_from_slice(b"opaque");
    +
    +        let parsed = OpenChannelMessage::parse(&mut payload.as_slice())
    +            .expect("unknown channel-open payload should remain opaque");
    +
    +        assert!(matches!(parsed.typ, ChannelType::Unknown { .. }));
    +    }
    +
    +    #[test]
    +    fn openssh_streamlocal_channel_open_reserved_fields_are_consumed() {
    +        let mut direct = channel_open_payload(b"direct-streamlocal@openssh.com");
    +        encode_string(&mut direct, b"/tmp/socket");
    +        encode_string(&mut direct, b"");
    +        push_u32(&mut direct, 0);
    +
    +        let parsed = OpenChannelMessage::parse(&mut direct.as_slice())
    +            .expect("direct streamlocal reserved fields should be consumed");
    +        assert!(matches!(parsed.typ, ChannelType::DirectStreamLocal(_)));
    +
    +        let mut forwarded = channel_open_payload(b"forwarded-streamlocal@openssh.com");
    +        encode_string(&mut forwarded, b"/tmp/socket");
    +        encode_string(&mut forwarded, b"");
    +
    +        let parsed = OpenChannelMessage::parse(&mut forwarded.as_slice())
    +            .expect("forwarded streamlocal reserved field should be consumed");
    +        assert!(matches!(parsed.typ, ChannelType::ForwardedStreamLocal(_)));
    +    }
    +}
    
  • russh/src/server/kex.rs+25 1 modified
    @@ -13,6 +13,7 @@ use crate::kex::dh::biguint_to_mpint;
     use crate::kex::{KexAlgorithm, KexAlgorithmImplementor, KexCause, KEXES};
     use crate::keys::key::PrivateKeyWithHashAlg;
     use crate::negotiation::{is_key_compatible_with_algo, Names, Select};
    +use crate::parsing::ensure_end;
     use crate::{msg, negotiation};
     
     thread_local! {
    @@ -173,7 +174,9 @@ impl ServerKex {
                     }
     
                     #[allow(clippy::indexing_slicing)] // length checked
    -                let gex_params = GexParams::decode(&mut &input.buffer[1..])?;
    +                let mut r = &input.buffer[1..];
    +                let gex_params = GexParams::decode(&mut r)?;
    +                ensure_end(&r)?;
                     debug!("client requests a gex group: {gex_params:?}");
     
                     let Some(dh_group) = handler.lookup_dh_gex_group(&gex_params).await? else {
    @@ -236,6 +239,7 @@ impl ServerKex {
                     self.exchange
                         .client_ephemeral
                         .extend_from_slice(&Bytes::decode(&mut r).map_err(Into::into)?);
    +                ensure_end(&r)?;
     
                     let exchange = &mut self.exchange;
                     kex.server_dh(exchange, &input.buffer)?;
    @@ -324,6 +328,8 @@ impl ServerKex {
                         );
                         return Err(Error::Kex.into());
                     }
    +                let r = &input.buffer[1..];
    +                ensure_end(&r)?;
     
                     debug!("new keys received");
                     Ok(KexProgress::Done {
    @@ -372,3 +378,21 @@ fn compute_keys(
             session_id: session_id_cv,
         })
     }
    +
    +#[cfg(test)]
    +mod tests {
    +    use crate::tests::raw_no_crypto::{
    +        assert_rejected, kexinit_payload, raw_kex_signal, timeout,
    +    };
    +
    +    #[tokio::test]
    +    async fn kexinit_with_trailing_bytes_rejected_by_server() {
    +        let result = timeout(raw_kex_signal(|payload| {
    +            payload.extend_from_slice(&kexinit_payload("none"));
    +            payload.push(0);
    +        }))
    +        .await;
    +
    +        assert_rejected(result, "server accepted a kexinit with trailing bytes");
    +    }
    +}
    
  • russh/src/tests.rs+452 0 modified
    @@ -714,6 +714,458 @@ mod server_kex_junk {
         }
     }
     
    +pub(crate) mod raw_no_crypto {
    +    use std::borrow::Cow;
    +    use std::io;
    +    use std::sync::{Arc, Mutex, OnceLock};
    +    use std::time::Duration;
    +
    +    use byteorder::{BigEndian, ByteOrder};
    +    use ssh_key::{Algorithm, PrivateKey};
    +    use tokio::io::{AsyncReadExt, AsyncWriteExt};
    +
    +    use super::*;
    +
    +    pub(crate) const MSG_SERVICE_REQUEST: u8 = 5;
    +    pub(crate) const MSG_SERVICE_ACCEPT: u8 = 6;
    +    pub(crate) const MSG_KEXINIT: u8 = 20;
    +    pub(crate) const MSG_NEWKEYS: u8 = 21;
    +    pub(crate) const MSG_USERAUTH_REQUEST: u8 = 50;
    +    pub(crate) const MSG_USERAUTH_FAILURE: u8 = 51;
    +    pub(crate) const MSG_USERAUTH_SUCCESS: u8 = 52;
    +    pub(crate) const MSG_CHANNEL_OPEN: u8 = 90;
    +    pub(crate) const MSG_CHANNEL_OPEN_CONFIRMATION: u8 = 91;
    +    pub(crate) const MSG_CHANNEL_REQUEST: u8 = 98;
    +
    +    pub(crate) async fn raw_service_request_signal(
    +        build_payload: impl FnOnce(&mut Vec<u8>),
    +    ) -> ServerSignal {
    +        let mut stream = RawSession::connect().await;
    +        let mut payload = Vec::new();
    +        build_payload(&mut payload);
    +        stream.send_packet(&payload).await.unwrap();
    +        stream.result().await
    +    }
    +
    +    pub(crate) async fn raw_auth_request_signal(
    +        build_payload: impl FnOnce(&mut Vec<u8>),
    +    ) -> ServerSignal {
    +        let mut stream = RawSession::connect().await;
    +        stream.service_request().await.unwrap();
    +
    +        let mut payload = Vec::new();
    +        build_payload(&mut payload);
    +        stream.send_packet(&payload).await.unwrap();
    +        stream.result().await
    +    }
    +
    +    pub(crate) async fn raw_kex_signal(build_payload: impl FnOnce(&mut Vec<u8>)) -> ServerSignal {
    +        let mut stream = RawSession::connect_without_kex().await;
    +
    +        let mut payload = Vec::new();
    +        build_payload(&mut payload);
    +        stream.send_packet(&payload).await.unwrap();
    +        stream.result().await
    +    }
    +
    +    pub(crate) async fn raw_channel_request_signal(
    +        build_payload: impl FnOnce(u32) -> Vec<u8>,
    +    ) -> ServerSignal {
    +        let mut stream = RawSession::connect().await;
    +        stream.auth_none().await.unwrap();
    +        let server_channel = stream.open_session().await.unwrap();
    +        stream
    +            .send_packet(&build_payload(server_channel))
    +            .await
    +            .unwrap();
    +        stream.result().await
    +    }
    +
    +    pub(crate) struct RawSession {
    +        pub(crate) events: Arc<Mutex<Vec<&'static str>>>,
    +        pub(crate) stream: tokio::net::TcpStream,
    +        pub(crate) server_task: tokio::task::JoinHandle<Result<(), Error>>,
    +    }
    +
    +    impl RawSession {
    +        pub(crate) async fn connect() -> Self {
    +            let mut stream = Self::connect_without_kex().await;
    +            raw_client_no_crypto_kex(&mut stream.stream).await.unwrap();
    +            stream
    +        }
    +
    +        pub(crate) async fn connect_without_kex() -> Self {
    +            let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
    +            let addr = listener.local_addr().unwrap();
    +            let events = Arc::new(Mutex::new(Vec::new()));
    +            let server_events = events.clone();
    +            let server_task = tokio::spawn(async move {
    +                let (socket, _) = listener.accept().await.unwrap();
    +                let running =
    +                    server::run_stream(
    +                        no_crypto_server_config(),
    +                        socket,
    +                        MalformedInputServer {
    +                            events: server_events,
    +                        },
    +                    )
    +                    .await
    +                    .unwrap();
    +                running.await
    +            });
    +
    +            let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
    +            stream.write_all(b"SSH-2.0-russh-test\r\n").await.unwrap();
    +            read_ssh_id(&mut stream).await.unwrap();
    +            let _server_kex = read_packet(&mut stream).await.unwrap();
    +            Self {
    +                events,
    +                stream,
    +                server_task,
    +            }
    +        }
    +
    +        pub(crate) async fn service_request(&mut self) -> io::Result<()> {
    +            let mut service = Vec::new();
    +            service.push(MSG_SERVICE_REQUEST);
    +            encode_string(&mut service, b"ssh-userauth");
    +            self.send_packet(&service).await?;
    +
    +            let accept = read_packet(&mut self.stream).await?;
    +            assert_eq!(accept.first(), Some(&MSG_SERVICE_ACCEPT));
    +            Ok(())
    +        }
    +
    +        pub(crate) async fn auth_none(&mut self) -> io::Result<()> {
    +            self.service_request().await?;
    +
    +            let mut auth = Vec::new();
    +            auth.push(MSG_USERAUTH_REQUEST);
    +            encode_string(&mut auth, b"test");
    +            encode_string(&mut auth, b"ssh-connection");
    +            encode_string(&mut auth, b"none");
    +            self.send_packet(&auth).await?;
    +
    +            let success = read_packet(&mut self.stream).await?;
    +            assert_eq!(success.first(), Some(&MSG_USERAUTH_SUCCESS));
    +            Ok(())
    +        }
    +
    +        pub(crate) async fn open_session(&mut self) -> io::Result<u32> {
    +            let mut open = Vec::new();
    +            open.push(MSG_CHANNEL_OPEN);
    +            encode_string(&mut open, b"session");
    +            push_u32(&mut open, 0);
    +            push_u32(&mut open, 1024 * 1024);
    +            push_u32(&mut open, 32 * 1024);
    +            self.send_packet(&open).await?;
    +
    +            let confirmation = read_packet(&mut self.stream).await?;
    +            assert_eq!(confirmation.first(), Some(&MSG_CHANNEL_OPEN_CONFIRMATION));
    +            Ok(BigEndian::read_u32(&confirmation[5..9]))
    +        }
    +
    +        pub(crate) async fn send_packet(&mut self, payload: &[u8]) -> io::Result<()> {
    +            self.stream.write_all(&ssh_packet(payload)).await?;
    +            self.stream.flush().await
    +        }
    +
    +        pub(crate) async fn result(mut self) -> ServerSignal {
    +            let result =
    +                tokio::time::timeout(Duration::from_millis(200), &mut self.server_task).await;
    +            let events = self.events.lock().unwrap().clone();
    +
    +            match result {
    +                Ok(Ok(Ok(()))) => ServerSignal::Closed { events },
    +                Ok(Ok(Err(_error))) => ServerSignal::ProtocolError { events },
    +                Ok(Err(join)) if join.is_panic() => ServerSignal::Panicked { events },
    +                Err(_) => {
    +                    self.server_task.abort();
    +                    ServerSignal::Survived { events }
    +                }
    +                _ => ServerSignal::Closed { events },
    +            }
    +        }
    +    }
    +
    +    fn no_crypto_server_config() -> Arc<server::Config> {
    +        let mut config = server::Config::default();
    +        config.inactivity_timeout = None;
    +        config.auth_rejection_time = Duration::from_millis(1);
    +        config.auth_rejection_time_initial = Some(Duration::from_millis(1));
    +        config.preferred = no_crypto_preferred();
    +        config
    +            .keys
    +            .push(PrivateKey::random(&mut rand::rng(), Algorithm::Ed25519).unwrap());
    +        Arc::new(config)
    +    }
    +
    +    fn no_crypto_preferred() -> Preferred {
    +        Preferred {
    +            kex: Cow::Owned(vec![kex::NONE]),
    +            key: Cow::Owned(vec![Algorithm::Ed25519]),
    +            cipher: Cow::Owned(vec![cipher::NONE]),
    +            mac: Cow::Owned(vec![mac::NONE]),
    +            compression: Cow::Owned(vec![compression::NONE]),
    +        }
    +    }
    +
    +    async fn raw_client_no_crypto_kex(stream: &mut tokio::net::TcpStream) -> io::Result<()> {
    +        stream
    +            .write_all(&ssh_packet(&kexinit_payload("none")))
    +            .await?;
    +        let newkeys = read_packet(stream).await?;
    +        assert_eq!(newkeys.first(), Some(&MSG_NEWKEYS));
    +        stream.write_all(&ssh_packet(&[MSG_NEWKEYS])).await?;
    +        stream.flush().await
    +    }
    +
    +    pub(crate) fn pty_req_payload(server_channel: u32, terminal_modes: &[u8]) -> Vec<u8> {
    +        let mut payload = channel_request_payload(server_channel, b"pty-req");
    +        encode_string(&mut payload, b"xterm");
    +        push_u32(&mut payload, 80);
    +        push_u32(&mut payload, 24);
    +        push_u32(&mut payload, 0);
    +        push_u32(&mut payload, 0);
    +        encode_string(&mut payload, terminal_modes);
    +        payload
    +    }
    +
    +    pub(crate) fn channel_open_payload(channel_type: &[u8]) -> Vec<u8> {
    +        let mut payload = Vec::new();
    +        encode_string(&mut payload, channel_type);
    +        push_u32(&mut payload, 0);
    +        push_u32(&mut payload, 1024 * 1024);
    +        push_u32(&mut payload, 32 * 1024);
    +        payload
    +    }
    +
    +    pub(crate) fn channel_request_payload(server_channel: u32, request_type: &[u8]) -> Vec<u8> {
    +        let mut payload = Vec::new();
    +        payload.push(MSG_CHANNEL_REQUEST);
    +        push_u32(&mut payload, server_channel);
    +        encode_string(&mut payload, request_type);
    +        payload.push(1);
    +        payload
    +    }
    +
    +    pub(crate) fn kexinit_payload(kex_name: &str) -> Vec<u8> {
    +        let mut payload = Vec::new();
    +        payload.push(MSG_KEXINIT);
    +        payload.extend_from_slice(&[0; 16]);
    +        encode_name_list(&mut payload, &[kex_name]);
    +        encode_name_list(&mut payload, &["ssh-ed25519"]);
    +        encode_name_list(&mut payload, &["none"]);
    +        encode_name_list(&mut payload, &["none"]);
    +        encode_name_list(&mut payload, &["none"]);
    +        encode_name_list(&mut payload, &["none"]);
    +        encode_name_list(&mut payload, &["none"]);
    +        encode_name_list(&mut payload, &["none"]);
    +        encode_name_list(&mut payload, &[]);
    +        encode_name_list(&mut payload, &[]);
    +        payload.push(0);
    +        push_u32(&mut payload, 0);
    +        payload
    +    }
    +
    +    fn ssh_packet(payload: &[u8]) -> Vec<u8> {
    +        let mut padding_len = 8 - ((5 + payload.len()) % 8);
    +        if padding_len < 4 {
    +            padding_len += 8;
    +        }
    +        let packet_len = 1 + payload.len() + padding_len;
    +        let mut packet = Vec::with_capacity(4 + packet_len);
    +        push_u32(&mut packet, packet_len as u32);
    +        packet.push(padding_len as u8);
    +        packet.extend_from_slice(payload);
    +        packet.resize(packet.len() + padding_len, 0);
    +        packet
    +    }
    +
    +    pub(crate) async fn read_packet(stream: &mut tokio::net::TcpStream) -> io::Result<Vec<u8>> {
    +        let mut len_buf = [0; 4];
    +        stream.read_exact(&mut len_buf).await?;
    +        let packet_len = BigEndian::read_u32(&len_buf) as usize;
    +        let mut packet = vec![0; packet_len];
    +        stream.read_exact(&mut packet).await?;
    +        let padding_len = packet[0] as usize;
    +        Ok(packet[1..packet.len() - padding_len].to_vec())
    +    }
    +
    +    async fn read_ssh_id(stream: &mut tokio::net::TcpStream) -> io::Result<Vec<u8>> {
    +        let mut id = Vec::new();
    +        loop {
    +            let mut byte = [0];
    +            stream.read_exact(&mut byte).await?;
    +            id.push(byte[0]);
    +            if byte[0] == b'\n' {
    +                return Ok(id);
    +            }
    +        }
    +    }
    +
    +    fn encode_name_list(buf: &mut Vec<u8>, names: &[&str]) {
    +        encode_string(buf, names.join(",").as_bytes());
    +    }
    +
    +    pub(crate) fn encode_string(buf: &mut Vec<u8>, value: &[u8]) {
    +        push_u32(buf, value.len() as u32);
    +        buf.extend_from_slice(value);
    +    }
    +
    +    pub(crate) fn push_u32(buf: &mut Vec<u8>, value: u32) {
    +        let mut bytes = [0; 4];
    +        BigEndian::write_u32(&mut bytes, value);
    +        buf.extend_from_slice(&bytes);
    +    }
    +
    +    pub(crate) async fn timeout(
    +        future: impl Future<Output = ServerSignal>,
    +    ) -> Result<ServerSignal, tokio::time::error::Elapsed> {
    +        tokio::time::timeout(Duration::from_secs(3), future).await
    +    }
    +
    +    pub(crate) async fn capture_panics<T>(future: impl Future<Output = T>) -> (T, bool) {
    +        static PANIC_HOOK_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    +
    +        let _guard = PANIC_HOOK_LOCK
    +            .get_or_init(|| Mutex::new(()))
    +            .lock()
    +            .unwrap();
    +        let panicked = Arc::new(std::sync::atomic::AtomicBool::new(false));
    +        let panicked_hook = panicked.clone();
    +        let previous_hook = std::panic::take_hook();
    +        std::panic::set_hook(Box::new(move |_| {
    +            panicked_hook.store(true, std::sync::atomic::Ordering::SeqCst);
    +        }));
    +
    +        let result = future.await;
    +
    +        std::panic::set_hook(previous_hook);
    +        (result, panicked.load(std::sync::atomic::Ordering::SeqCst))
    +    }
    +
    +    #[derive(Debug)]
    +    pub(crate) enum ServerSignal {
    +        Closed { events: Vec<&'static str> },
    +        ProtocolError { events: Vec<&'static str> },
    +        Panicked { events: Vec<&'static str> },
    +        Survived { events: Vec<&'static str> },
    +    }
    +
    +    impl ServerSignal {
    +        pub(crate) fn events(&self) -> &[&'static str] {
    +            match self {
    +                Self::Closed { events }
    +                | Self::ProtocolError { events }
    +                | Self::Panicked { events }
    +                | Self::Survived { events } => events,
    +            }
    +        }
    +    }
    +
    +    pub(crate) fn assert_rejected(
    +        result: Result<ServerSignal, tokio::time::error::Elapsed>,
    +        message: &str,
    +    ) {
    +        assert!(
    +            matches!(
    +                result,
    +                Ok(ServerSignal::Closed { .. } | ServerSignal::ProtocolError { .. })
    +            ),
    +            "{message}: {result:?}; handler events: {:?}",
    +            result.as_ref().ok().map(ServerSignal::events).unwrap_or(&[])
    +        );
    +    }
    +
    +    #[derive(Clone)]
    +    struct MalformedInputServer {
    +        events: Arc<Mutex<Vec<&'static str>>>,
    +    }
    +
    +    impl MalformedInputServer {
    +        fn record(&self, event: &'static str) {
    +            self.events.lock().unwrap().push(event);
    +        }
    +    }
    +
    +    impl server::Handler for MalformedInputServer {
    +        type Error = Error;
    +
    +        async fn auth_none(&mut self, _user: &str) -> Result<server::Auth, Self::Error> {
    +            self.record("auth_none");
    +            Ok(server::Auth::Accept)
    +        }
    +
    +        async fn auth_password(
    +            &mut self,
    +            _user: &str,
    +            _password: &str,
    +        ) -> Result<server::Auth, Self::Error> {
    +            self.record("auth_password");
    +            Ok(server::Auth::Reject {
    +                proceed_with_methods: None,
    +                partial_success: false,
    +            })
    +        }
    +
    +        async fn channel_open_session(
    +            &mut self,
    +            _channel: Channel<server::Msg>,
    +            _session: &mut server::Session,
    +        ) -> Result<bool, Self::Error> {
    +            self.record("channel_open_session");
    +            Ok(true)
    +        }
    +
    +        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 server::Session,
    +        ) -> Result<(), Self::Error> {
    +            self.record("pty_request");
    +            Ok(())
    +        }
    +
    +        async fn env_request(
    +            &mut self,
    +            _channel: ChannelId,
    +            _variable_name: &str,
    +            _variable_value: &str,
    +            _session: &mut server::Session,
    +        ) -> Result<(), Self::Error> {
    +            self.record("env_request");
    +            Ok(())
    +        }
    +
    +        async fn exec_request(
    +            &mut self,
    +            _channel: ChannelId,
    +            _data: &[u8],
    +            _session: &mut server::Session,
    +        ) -> Result<(), Self::Error> {
    +            self.record("exec_request");
    +            Ok(())
    +        }
    +
    +        async fn signal(
    +            &mut self,
    +            _channel: ChannelId,
    +            _signal: Sig,
    +            _session: &mut server::Session,
    +        ) -> Result<(), Self::Error> {
    +            self.record("signal");
    +            Ok(())
    +        }
    +    }
    +}
    +
     /// Integration test for FutureCertificate authentication flow
     #[cfg(unix)]
     mod future_certificate {
    
72b250aa2dac

Migrate to upstream ssh-key and update crypto crates (#709)

https://github.com/Eugeny/russhEugeneMay 20, 2026Fixed in 0.61.0via llm-release-walk
24 files changed · +365 242
  • Cargo.toml+11 10 modified
    @@ -3,30 +3,31 @@ members = ["russh", "russh-config", "cryptovec", "pageant", "russh-util"]
     resolver = "2"
     
     [workspace.dependencies]
    -aes = "0.8"
    +aes = "0.9"
     async-trait = "0.1.50"
    +base16ct = "1"
     byteorder = "1.4"
     bytes = "1.7"
    -digest = "0.10"
    +digest = "0.11.0-rc.5"
     delegate = "0.13"
     env_logger = "0.11"
     futures = "0.3"
    -hmac = "0.12"
    +hmac = "0.13"
     log = "0.4.11"
     rand = { version = "0.10", features = ["thread_rng"] }
    -sha1 = { version = "0.10.5", features = ["oid"] }
    -sha2 = { version = "0.10.6", features = ["oid"] }
    +sha1 = { version = "0.11", features = ["oid"] }
    +sha2 = { version = "0.11", features = ["oid"] }
     signature = "3.0.0-rc.10"
    -ssh-encoding = { version = "0.2", features = ["bytes"] }
    -ssh-key = { version = "=0.6.18", features = [
    +ssh-encoding = { version = "=0.3.0-rc.9", features = ["bytes"] }
    +ssh-key = { version = "=0.7.0-rc.10", features = [
         "ed25519",
         "p256",
         "p384",
         "p521",
         "encryption",
         "ppk",
    -    "hazmat-allow-insecure-rsa-keys",
    -], package = "internal-russh-forked-ssh-key" }
    +    "sha1",
    +] }
     thiserror = "2.0.18"
     tokio = { version = "1.17.0" }
    -tokio-stream = { version = "0.1.3", features = ["net", "sync"] }
    \ No newline at end of file
    +tokio-stream = { version = "0.1.3", features = ["net", "sync"] }
    
  • cryptovec/src/ssh.rs+1 11 modified
    @@ -1,17 +1,7 @@
    -use ssh_encoding::{Reader, Result, Writer};
    +use ssh_encoding::{Result, Writer};
     
     use crate::CryptoVec;
     
    -impl Reader for CryptoVec {
    -    fn read<'o>(&mut self, out: &'o mut [u8]) -> Result<&'o [u8]> {
    -        (&self[..]).read(out)
    -    }
    -
    -    fn remaining_len(&self) -> usize {
    -        self.len()
    -    }
    -}
    -
     impl Writer for CryptoVec {
         fn write(&mut self, bytes: &[u8]) -> Result<()> {
             self.extend(bytes);
    
  • .gitignore+1 0 modified
    @@ -5,3 +5,4 @@ target
     Cargo.lock
     .cargo-ok
     ca-test*
    +/.vscode
    
  • pageant/Cargo.toml+1 1 modified
    @@ -13,6 +13,7 @@ rust-version = "1.85"
     thiserror.workspace = true
     
     [target.'cfg(windows)'.dependencies]
    +base16ct = { workspace = true, features = ["alloc"] }
     futures.workspace = true
     rand.workspace = true
     byteorder.workspace = true
    @@ -45,4 +46,3 @@ namedpipes = [
         "windows/Win32_Security_Authentication_Identity",
         "windows/Win32_Security_Cryptography",
     ]
    -
    
  • pageant/src/namedpipes.rs+2 1 modified
    @@ -3,6 +3,7 @@ use std::pin::Pin;
     use std::task::{Context, Poll};
     use std::time::Duration;
     
    +use base16ct::lower;
     use delegate::delegate;
     use log::debug;
     use sha2::{Digest, Sha256};
    @@ -109,7 +110,7 @@ impl PageantStream {
             let mut hasher = Sha256::new();
             hasher.update((cryptdata.len() as u32).to_be_bytes());
             hasher.update(&cryptdata);
    -        Ok(format!("{:x}", hasher.finalize()))
    +        Ok(lower::encode_string(&hasher.finalize()))
         }
     }
     
    
  • russh/Cargo.toml+6 18 modified
    @@ -23,36 +23,28 @@ des = ["dep:des"]
     # Danger: DSA algorithm is insecure.
     dsa = ["ssh-key/dsa"]
     ring = ["dep:ring"] # Alternative crypto backend.
    -rsa = ["dep:rsa", "dep:pkcs1", "ssh-key/rsa", "ssh-key/rsa-sha1"]
    +rsa = ["dep:rsa", "dep:pkcs1", "ssh-key/rsa"]
     serde = ["ssh-key/serde"]
     _bench = ["dep:criterion"]
     
     [dependencies]
    -# Compatibility floor pins for cargo-minimal-versions. These are otherwise
    -# transitive members of the RustCrypto prerelease stack; some are renamed to
    -# coexist with older direct dependency lines.
    -aead_0_6 = { package = "aead", version = "=0.6.0-rc.10" }
     aes.workspace = true
    -aes_0_9 = { package = "aes", version = "0.9.0" }
    -aes_gcm_0_11 = { package = "aes-gcm", version = "=0.11.0-rc.3" }
     async-trait = { workspace = true, optional = true }
     aws-lc-rs = { version = "1.16.2", optional = true }
     bitflags = "2.0"
    -block-padding = { version = "0.3", features = ["std"] }
    +block-padding = { version = "0.4" }
     byteorder.workspace = true
     bytes.workspace = true
    -cbc = { version = "0.1" }
    -cbc_0_2 = { package = "cbc", version = "0.2.0" }
    +cbc = { version = "0.2" }
     cipher = "0.5.1" # only pinned due to a cargo-minimal-versions failure in 0.5.0
    -ctr = "0.9.2"
    -ctr_0_10 = { package = "ctr", version = "0.10.0" }
    +ctr = "0.10"
     curve25519-dalek = "=5.0.0-pre.6"
     crypto-bigint = { version = "0.7.3", features = ["alloc"] }
     data-encoding = "2.3"
     delegate.workspace = true
     digest.workspace = true
     der = "0.8"
    -des = { version = "0.8.1", optional = true }
    +des = { version = "0.9", optional = true }
     ecdsa = "=0.17.0-rc.18"
     ed25519-dalek = { version = "=3.0.0-pre.7", features = ["alloc", "rand_core", "pkcs8"] }
     elliptic-curve = { version = "=0.14.0-rc.32", features = ["ecdh"] }
    @@ -65,7 +57,6 @@ ghash = "0.6.0" # only pinned due to a cargo-minimal-versions failure in 0.6.0-r
     hex-literal = "1"
     hkdf = "0.13.0"
     hmac.workspace = true
    -hmac_0_13 = { package = "hmac", version = "0.13.0" }
     inout = { version = "0.1", features = ["std"] }
     keccak = "0.2.0"
     log.workspace = true
    @@ -81,8 +72,7 @@ num_bigint_0_4 = { package = "num-bigint", version = "0.4.6" }
     p256 = { version = "=0.14.0-rc.9", features = ["ecdh"] }
     p384 = { version = "=0.14.0-rc.9", features = ["ecdh"] }
     p521 = { version = "=0.14.0-rc.9", features = ["ecdh"] }
    -pbkdf2 = "0.12"
    -pbkdf2_0_13 = { package = "pbkdf2", version = "0.13.0" }
    +pbkdf2 = "0.13"
     pkcs1 = { version = "=0.8.0-rc.4", optional = true }
     pkcs5 = "0.8"
     pkcs8 = { version = "0.11", features = ["encryption", "std"] }
    @@ -99,9 +89,7 @@ salsa20 = "0.11.0"
     scrypt = "0.12.0"
     sec1 = { version = "0.8", features = ["der"] }
     sha1.workspace = true
    -sha1_0_11 = { package = "sha1", version = "0.11.0" }
     sha2.workspace = true
    -sha2_0_11 = { package = "sha2", version = "0.11.0" }
     sha3 = "0.11.0"
     signature.workspace = true
     spki = "0.8"
    
  • russh/src/auth.rs+0 1 modified
    @@ -100,7 +100,6 @@ impl From<&NameList> for MethodSet {
         fn from(value: &NameList) -> Self {
             Self(
                 value
    -                .0
                     .iter()
                     .filter_map(|x| MethodKind::from_str(x).ok())
                     .collect(),
    
  • russh/src/cipher/block.rs+80 20 modified
    @@ -14,32 +14,91 @@
     use std::convert::TryInto;
     use std::marker::PhantomData;
     
    -use aes::cipher::{IvSizeUser, KeyIvInit, KeySizeUser, StreamCipher};
    +use aes::cipher::{
    +    InOutBuf, Iv, IvSizeUser, Key, KeyIvInit, KeySizeUser, StreamCipher, StreamCipherError,
    +    StreamCipherSeek,
    +};
     #[allow(deprecated)]
    -use digest::generic_array::GenericArray as GenericArray_0_14;
     use rand_core::Rng;
     
     use super::super::Error;
     use super::PACKET_LENGTH_LEN;
     use crate::keys::key::safe_rng;
     use crate::mac::{Mac, MacAlgorithm};
     
    -// Allow deprecated generic-array 0.14 usage until RustCrypto crates (cipher, digest, etc.)
    -// upgrade to generic-array 1.x. Remove this when dependencies no longer use 0.14.
    -#[allow(deprecated)]
     fn new_cipher_from_slices<C: KeyIvInit>(k: &[u8], n: &[u8]) -> C {
    +    #[allow(clippy::expect_used)]
         C::new(
    -        GenericArray_0_14::from_slice(k),
    -        GenericArray_0_14::from_slice(n),
    +        <&Key<C>>::try_from(k).expect("key length matches"),
    +        <&Iv<C>>::try_from(n).expect("iv length matches"),
         )
     }
     
    +/// Cloneable wrapper for `Ctr128BE<>`
    +pub struct CtrWrapper<C>
    +where
    +    C: KeyIvInit,
    +{
    +    key: Key<C>,
    +    initial_iv: Iv<C>,
    +    pos: u64,
    +}
    +
    +impl<C: KeyIvInit> Clone for CtrWrapper<C> {
    +    fn clone(&self) -> Self {
    +        Self {
    +            key: self.key.clone(),
    +            initial_iv: self.initial_iv.clone(),
    +            pos: self.pos,
    +        }
    +    }
    +}
    +
    +impl<C: KeyIvInit> KeySizeUser for CtrWrapper<C> {
    +    type KeySize = <C as KeySizeUser>::KeySize;
    +}
    +
    +impl<C: KeyIvInit> IvSizeUser for CtrWrapper<C> {
    +    type IvSize = <C as IvSizeUser>::IvSize;
    +}
    +
    +impl<C: KeyIvInit> KeyIvInit for CtrWrapper<C> {
    +    fn new(key: &Key<Self>, iv: &Iv<Self>) -> Self {
    +        Self {
    +            key: key.clone(),
    +            initial_iv: iv.clone(),
    +            pos: 0,
    +        }
    +    }
    +}
    +
    +impl<C: KeyIvInit + StreamCipher + StreamCipherSeek> StreamCipher for CtrWrapper<C> {
    +    fn check_remaining(&self, _data_len: usize) -> Result<(), StreamCipherError> {
    +        Ok(())
    +    }
    +
    +    fn unchecked_apply_keystream_inout(&mut self, buf: InOutBuf<'_, '_, u8>) {
    +        let mut cipher = C::new(&self.key, &self.initial_iv);
    +        cipher.seek(self.pos);
    +        cipher.unchecked_apply_keystream_inout(buf);
    +        self.pos = cipher.current_pos();
    +    }
    +
    +    fn unchecked_write_keystream(&mut self, buf: &mut [u8]) {
    +        let mut cipher = C::new(&self.key, &self.initial_iv);
    +        cipher.seek(self.pos);
    +        cipher.unchecked_write_keystream(buf);
    +        self.pos = cipher.current_pos();
    +    }
    +}
    +
     pub struct SshBlockCipher<C: BlockStreamCipher + PacketLengthProbe + KeySizeUser + IvSizeUser>(
         pub PhantomData<C>,
     );
     
    -impl<C: BlockStreamCipher + PacketLengthProbe + KeySizeUser + IvSizeUser + KeyIvInit + Send + 'static>
    -    super::Cipher for SshBlockCipher<C>
    +impl<
    +    C: BlockStreamCipher + PacketLengthProbe + KeySizeUser + IvSizeUser + KeyIvInit + Send + 'static,
    +> super::Cipher for SshBlockCipher<C>
     {
         fn key_len(&self) -> usize {
             C::key_size()
    @@ -237,15 +296,15 @@ impl<T: StreamCipher + Clone> PacketLengthProbe for T {
     
     #[cfg(test)]
     mod tests {
    +    use aes::Aes128;
         use aes::cipher::KeyIvInit;
         use aes::cipher::StreamCipher;
    -    use aes::Aes128;
         use aes::cipher::{IvSizeUser, KeySizeUser};
         use ctr::Ctr128BE;
         use digest::typenum::U16;
         use tokio::io::AsyncWriteExt;
     
    -    use super::{BlockStreamCipher, OpeningKey, PacketLengthProbe};
    +    use super::{BlockStreamCipher, CtrWrapper, OpeningKey, PacketLengthProbe};
         use crate::mac::MacAlgorithm;
         use crate::sshbuffer::SSHBuffer;
     
    @@ -255,11 +314,11 @@ mod tests {
             let key = fixture_bytes::<16>(7);
             let iv = fixture_bytes::<16>(3);
     
    -        let mut encryptor = Ctr128BE::<Aes128>::new(&key.into(), &iv.into());
    +        let mut encryptor = CtrWrapper::<Ctr128BE<Aes128>>::new(&key.into(), &iv.into());
             let mut ciphertext = plaintext;
             encryptor.apply_keystream(&mut ciphertext);
     
    -        let cipher = Ctr128BE::<Aes128>::new(&key.into(), &iv.into());
    +        let cipher = CtrWrapper::<Ctr128BE<Aes128>>::new(&key.into(), &iv.into());
             let mut probed_block = ciphertext;
             cipher.decrypt_packet_length_block(&mut probed_block);
             assert_eq!(probed_block, plaintext);
    @@ -281,13 +340,14 @@ mod tests {
             };
             let mut opening = opening;
             let mut buffer = SSHBuffer::new();
    -        let bytes_read = runtime.block_on(async {
    -            let (mut writer, mut reader) = tokio::io::duplex(64);
    -            writer.write_all(&[0; 17]).await?;
    -            drop(writer);
    -            crate::cipher::read(&mut reader, &mut buffer, &mut opening).await
    -        })
    -        .map_err(std::io::Error::other)?;
    +        let bytes_read = runtime
    +            .block_on(async {
    +                let (mut writer, mut reader) = tokio::io::duplex(64);
    +                writer.write_all(&[0; 17]).await?;
    +                drop(writer);
    +                crate::cipher::read(&mut reader, &mut buffer, &mut opening).await
    +            })
    +            .map_err(std::io::Error::other)?;
     
             assert_eq!(bytes_read, 16);
             Ok(())
    
  • russh/src/cipher/cbc.rs+46 43 modified
    @@ -1,86 +1,89 @@
    -use aes::cipher::{
    -    BlockCipher, BlockDecrypt, BlockDecryptMut, BlockEncrypt, BlockEncryptMut, InnerIvInit, Iv,
    -    IvSizeUser,
    -};
    +use cbc::cipher::{InnerIvInit, Iv, IvSizeUser};
     use cbc::{Decryptor, Encryptor};
    -use digest::crypto_common::InnerUser;
    -#[allow(deprecated)]
    -use digest::generic_array::GenericArray;
    +use cipher::common::InnerUser;
    +use cipher::{
    +    Block, BlockCipherDecrypt, BlockCipherEncrypt, BlockModeDecrypt, BlockModeEncrypt, IvState,
    +};
     
     use super::block::{BlockStreamCipher, PacketLengthProbe};
     
    -// Allow deprecated generic-array 0.14 usage until RustCrypto crates (cipher, cbc, etc.)
    -// upgrade to generic-array 1.x. Remove this when dependencies no longer use 0.14.
    -#[allow(deprecated)]
    -fn generic_array_from_slice<N>(chunk: &[u8]) -> GenericArray<u8, N>
    +/// CBC wrapper that stores the decryption cipher and IV separately rather than
    +/// a `cbc::Decryptor`, because `Decryptor` is no longer `Clone` in cbc 0.2.
    +/// This allows stateless peeking at the packet length block without cloning.
    +pub struct CbcWrapper<C>
     where
    -    N: digest::generic_array::ArrayLength<u8>,
    +    C: BlockCipherEncrypt + BlockCipherDecrypt,
     {
    -    GenericArray::from_slice(chunk).clone()
    +    encryptor: Encryptor<C>,
    +    /// Raw cipher used for decryption. `BlockCipherDecrypt::decrypt_block` takes
    +    /// `&self`, so this can be used without mutation for packet-length probing.
    +    dec_cipher: C,
    +    /// Current CBC decryption IV (i.e. the last ciphertext block consumed).
    +    dec_iv: Block<C>,
     }
     
    -pub struct CbcWrapper<C: BlockEncrypt + BlockCipher + BlockDecrypt> {
    -    encryptor: Encryptor<C>,
    -    decryptor: Decryptor<C>,
    +impl<C> CbcWrapper<C>
    +where
    +    C: BlockCipherEncrypt + BlockCipherDecrypt + Clone,
    +{
    +    #[must_use]
    +    fn decrypt_inner(&self, data: &mut [u8]) -> Iv<Self> {
    +        let mut dec = Decryptor::<&C>::inner_iv_init(&self.dec_cipher, &self.dec_iv);
    +
    +        for chunk in data.chunks_exact_mut(C::block_size()) {
    +            #[allow(clippy::expect_used)]
    +            let block = <&mut Block<C>>::try_from(chunk).expect("chunk length matches block size");
    +
    +            dec.decrypt_block(block);
    +        }
    +
    +        dec.iv_state()
    +    }
     }
     
    -impl<C: BlockEncrypt + BlockCipher + BlockDecrypt> InnerUser for CbcWrapper<C> {
    +impl<C: BlockCipherEncrypt + BlockCipherDecrypt> InnerUser for CbcWrapper<C> {
         type Inner = C;
     }
     
    -impl<C: BlockEncrypt + BlockCipher + BlockDecrypt> IvSizeUser for CbcWrapper<C> {
    +impl<C: BlockCipherEncrypt + BlockCipherDecrypt> IvSizeUser for CbcWrapper<C> {
         type IvSize = C::BlockSize;
     }
     
    -impl<C: BlockEncrypt + BlockCipher + BlockDecrypt> BlockStreamCipher for CbcWrapper<C> {
    +impl<C: BlockCipherEncrypt + BlockCipherDecrypt + Clone> BlockStreamCipher for CbcWrapper<C> {
         fn encrypt_data(&mut self, data: &mut [u8]) {
             for chunk in data.chunks_exact_mut(C::block_size()) {
    -            let mut block = generic_array_from_slice(chunk);
    -            self.encryptor.encrypt_block_mut(&mut block);
    -            chunk.copy_from_slice(&block);
    +            #[allow(clippy::expect_used)]
    +            let block = <&mut Block<C>>::try_from(chunk).expect("chunk length matches block size");
    +            self.encryptor.encrypt_block(block);
             }
         }
     
         fn decrypt_data(&mut self, data: &mut [u8]) {
    -        for chunk in data.chunks_exact_mut(C::block_size()) {
    -            let mut block = generic_array_from_slice(chunk);
    -            self.decryptor.decrypt_block_mut(&mut block);
    -            chunk.copy_from_slice(&block);
    -        }
    +        self.dec_iv = self.decrypt_inner(data)
         }
     }
     
    -impl<C: BlockEncrypt + BlockCipher + BlockDecrypt + Clone> PacketLengthProbe for CbcWrapper<C>
    -where
    -    C: BlockDecryptMut,
    -{
    +impl<C: BlockCipherEncrypt + BlockCipherDecrypt + Clone> PacketLengthProbe for CbcWrapper<C> {
         fn decrypt_packet_length_block(&self, first_block: &mut [u8; 16]) {
    -        let mut decryptor = self.decryptor.clone();
    -        for chunk in first_block.chunks_exact_mut(C::block_size()) {
    -            let mut block = generic_array_from_slice(chunk);
    -            decryptor.decrypt_block_mut(&mut block);
    -            chunk.copy_from_slice(&block);
    -        }
    +        let _ = self.decrypt_inner(first_block);
         }
     }
     
    -impl<C: BlockEncrypt + BlockCipher + BlockDecrypt + Clone> InnerIvInit for CbcWrapper<C>
    -where
    -    C: BlockEncryptMut + BlockCipher,
    -{
    +impl<C: BlockCipherEncrypt + BlockCipherDecrypt + Clone> InnerIvInit for CbcWrapper<C> {
         #[inline]
         fn inner_iv_init(cipher: C, iv: &Iv<Self>) -> Self {
             Self {
                 encryptor: Encryptor::inner_iv_init(cipher.clone(), iv),
    -            decryptor: Decryptor::inner_iv_init(cipher, iv),
    +            dec_cipher: cipher,
    +            dec_iv: iv.clone(),
             }
         }
     }
     
     #[cfg(test)]
     mod tests {
    -    use aes::cipher::KeyIvInit;
         use aes::Aes128;
    +    use cbc::cipher::KeyIvInit;
         #[cfg(feature = "des")]
         use des::TdesEde3;
     
    
  • russh/src/cipher/mod.rs+4 3 modified
    @@ -26,6 +26,7 @@ use aes::{Aes128, Aes192, Aes256};
     #[cfg(feature = "aws-lc-rs")]
     use aws_lc_rs::aead::{AES_128_GCM as ALGORITHM_AES_128_GCM, AES_256_GCM as ALGORITHM_AES_256_GCM};
     use byteorder::{BigEndian, ByteOrder};
    +use block::CtrWrapper;
     use ctr::Ctr128BE;
     use delegate::delegate;
     use log::trace;
    @@ -103,9 +104,9 @@ pub const NONE: Name = Name("none");
     pub(crate) static _CLEAR: Clear = Clear {};
     #[cfg(feature = "des")]
     static _3DES_CBC: SshBlockCipher<CbcWrapper<des::TdesEde3>> = SshBlockCipher(PhantomData);
    -static _AES_128_CTR: SshBlockCipher<Ctr128BE<Aes128>> = SshBlockCipher(PhantomData);
    -static _AES_192_CTR: SshBlockCipher<Ctr128BE<Aes192>> = SshBlockCipher(PhantomData);
    -static _AES_256_CTR: SshBlockCipher<Ctr128BE<Aes256>> = SshBlockCipher(PhantomData);
    +static _AES_128_CTR: SshBlockCipher<CtrWrapper<Ctr128BE<Aes128>>> = SshBlockCipher(PhantomData);
    +static _AES_192_CTR: SshBlockCipher<CtrWrapper<Ctr128BE<Aes192>>> = SshBlockCipher(PhantomData);
    +static _AES_256_CTR: SshBlockCipher<CtrWrapper<Ctr128BE<Aes256>>> = SshBlockCipher(PhantomData);
     static _AES_128_GCM: GcmCipher = GcmCipher(&ALGORITHM_AES_128_GCM);
     static _AES_256_GCM: GcmCipher = GcmCipher(&ALGORITHM_AES_256_GCM);
     static _AES_128_CBC: SshBlockCipher<CbcWrapper<Aes128>> = SshBlockCipher(PhantomData);
    
  • russh/src/client/encrypted.rs+1 2 modified
    @@ -341,8 +341,7 @@ impl Session {
             let algs = NameList::decode(r)?;
             debug!("* server-sig-algs");
             self.server_sig_algs = Some(
    -            algs.0
    -                .iter()
    +            algs.iter()
                     .filter_map(|x| Algorithm::from_str(x).ok())
                     .inspect(|x| {
                         debug!("  * {x:?}");
    
  • russh/src/client/kex.rs+2 0 modified
    @@ -343,6 +343,8 @@ impl ClientKex {
                         );
                         return Err(Error::Kex);
                     }
    +
    +                #[allow(clippy::indexing_slicing, reason = "length checked")]
                     let r = &input.buffer[1..];
                     ensure_end(&r)?;
     
    
  • russh/src/client/mod.rs+1 0 modified
    @@ -1135,6 +1135,7 @@ impl Session {
                         // The kex signal has not been consumed yet,
                         // so we can send return the concrete error to be propagated
                         // into the JoinHandle and returned from `connect_stream`
    +                    debug!("disconnected during handshake {e:?}");
                         Err(e)
                     } else {
                         // The kex signal has been consumed, so no one is
    
  • russh/src/helpers.rs+94 22 modified
    @@ -15,43 +15,115 @@ impl<E: Encode> EncodedExt for E {
         }
     }
     
    -pub struct NameList(pub Vec<String>);
    +mod limited_string {
    +    use super::*;
    +    use std::ops::Deref;
     
    -impl Debug for NameList {
    -    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    -        self.0.fmt(f)
    -    }
    -}
    +    pub struct LimitedString<const N: usize>(String);
    +
    +    impl<const N: usize> Deref for LimitedString<N> {
    +        type Target = String;
     
    -impl NameList {
    -    pub fn as_encoded_string(&self) -> String {
    -        self.0.join(",")
    +        fn deref(&self) -> &Self::Target {
    +            &self.0
    +        }
         }
     
    -    pub fn from_encoded_string(value: &str) -> Self {
    -        Self(value.split(',').map(|x| x.to_string()).collect())
    +    impl<const N: usize> Decode for LimitedString<N> {
    +        type Error = ssh_encoding::Error;
    +
    +        fn decode(reader: &mut impl ssh_encoding::Reader) -> Result<Self, Self::Error> {
    +            reader.read_prefixed(|reader| {
    +                let len = reader.remaining_len();
    +                if len > N {
    +                    return Err(ssh_encoding::Error::Length);
    +                }
    +
    +                // Allocate only after the SSH string length has been bounded.
    +                let mut buf = vec![0; len];
    +                reader.read(&mut buf)?;
    +                let value =
    +                    String::from_utf8(buf).map_err(|_| ssh_encoding::Error::CharacterEncoding)?;
    +                reader.ensure_finished()?;
    +
    +                Ok(Self(value))
    +            })
    +        }
         }
     }
     
    -impl Encode for NameList {
    -    fn encoded_len(&self) -> Result<usize, ssh_encoding::Error> {
    -        self.as_encoded_string().encoded_len()
    +mod name_list {
    +    use std::ops::Deref;
    +
    +    use super::*;
    +    const MAX_NAME_LIST_ENTRIES: usize = 1024;
    +    const MAX_NAME_LIST_BYTES: usize = 16 * 1024;
    +
    +    pub struct NameList(pub Vec<String>);
    +
    +    impl Debug for NameList {
    +        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    +            self.0.fmt(f)
    +        }
         }
     
    -    fn encode(&self, writer: &mut impl ssh_encoding::Writer) -> Result<(), ssh_encoding::Error> {
    -        self.as_encoded_string().encode(writer)
    +    impl Deref for NameList {
    +        type Target = [String];
    +
    +        fn deref(&self) -> &Self::Target {
    +            &self.0
    +        }
         }
    -}
     
    -impl Decode for NameList {
    -    fn decode(reader: &mut impl ssh_encoding::Reader) -> Result<Self, ssh_encoding::Error> {
    -        let s = String::decode(reader)?;
    -        Ok(Self::from_encoded_string(&s))
    +    impl NameList {
    +        pub fn as_encoded_string(&self) -> String {
    +            self.0.join(",")
    +        }
    +
    +        pub fn from_encoded_string(value: &str) -> Result<Self, ssh_encoding::Error> {
    +            Ok(Self(value.split(',').try_fold(
    +                Vec::new(),
    +                |mut list, name| {
    +                    if name.is_empty() || !name.is_ascii() {
    +                        return Err(ssh_encoding::Error::CharacterEncoding);
    +                    }
    +                    if list.len() > MAX_NAME_LIST_ENTRIES {
    +                        Err(ssh_encoding::Error::Length)
    +                    } else {
    +                        list.push(name.into());
    +                        Ok(list)
    +                    }
    +                },
    +            )?))
    +        }
    +    }
    +
    +    impl Encode for NameList {
    +        fn encoded_len(&self) -> Result<usize, ssh_encoding::Error> {
    +            self.as_encoded_string().encoded_len()
    +        }
    +
    +        fn encode(
    +            &self,
    +            writer: &mut impl ssh_encoding::Writer,
    +        ) -> Result<(), ssh_encoding::Error> {
    +            self.as_encoded_string().encode(writer)
    +        }
         }
     
    -    type Error = ssh_encoding::Error;
    +    impl Decode for NameList {
    +        fn decode(reader: &mut impl ssh_encoding::Reader) -> Result<Self, ssh_encoding::Error> {
    +            let s = LimitedString::<MAX_NAME_LIST_BYTES>::decode(reader)?;
    +            Self::from_encoded_string(&s)
    +        }
    +
    +        type Error = ssh_encoding::Error;
    +    }
     }
     
    +pub use limited_string::LimitedString;
    +pub use name_list::NameList;
    +
     pub(crate) mod macros {
         #[allow(clippy::crate_in_macro_def)]
         macro_rules! map_err {
    
  • russh/src/kex/curve25519.rs+6 4 modified
    @@ -7,11 +7,11 @@ use sha2::Digest;
     use ssh_encoding::{Encode, Writer};
     
     use super::{
    -    compute_keys, encode_mpint, KexAlgorithm, KexAlgorithmImplementor, KexType, SharedSecret,
    +    KexAlgorithm, KexAlgorithmImplementor, KexType, SharedSecret, compute_keys, encode_mpint,
     };
     use crate::mac::{self};
     use crate::session::Exchange;
    -use crate::{cipher, msg, CryptoVec};
    +use crate::{CryptoVec, cipher, msg};
     
     pub struct Curve25519KexType {}
     
    @@ -79,7 +79,9 @@ impl KexAlgorithmImplementor for Curve25519Kex {
     
             // fill exchange.
             exchange.server_ephemeral.clear();
    -        exchange.server_ephemeral.extend_from_slice(&server_pubkey.0);
    +        exchange
    +            .server_ephemeral
    +            .extend_from_slice(&server_pubkey.0);
             let shared = server_secret * client_pubkey;
             self.shared_secret = Some(shared);
             Ok(())
    @@ -99,7 +101,7 @@ impl KexAlgorithmImplementor for Curve25519Kex {
             client_ephemeral.extend_from_slice(&client_pubkey.0);
     
             msg::KEX_ECDH_INIT.encode(writer)?;
    -        client_pubkey.0.encode(writer)?;
    +        (client_pubkey.0[..]).encode(writer)?;
     
             self.local_secret = Some(client_secret);
             Ok(())
    
  • russh/src/keys/format/pkcs5.rs+2 3 modified
    @@ -11,8 +11,7 @@ pub fn decode_pkcs5(
         password: Option<&str>,
         enc: Encryption,
     ) -> Result<PrivateKey, Error> {
    -    use aes::cipher::{BlockDecryptMut, KeyIvInit};
    -    use block_padding::Pkcs7;
    +    use aes::cipher::{block_padding::Pkcs7, BlockModeDecrypt, KeyIvInit};
     
         if let Some(pass) = password {
             let sec = match enc {
    @@ -25,7 +24,7 @@ pub fn decode_pkcs5(
                     #[allow(clippy::unwrap_used)] // AES parameters are static
                     let c = cbc::Decryptor::<Aes128>::new_from_slices(&md5.0, &iv[..]).unwrap();
                     let mut dec = secret.to_vec();
    -                c.decrypt_padded_mut::<Pkcs7>(&mut dec)?.to_vec()
    +                c.decrypt_padded::<Pkcs7>(&mut dec)?.to_vec()
                 }
                 Encryption::Aes256Cbc(_) => unimplemented!(),
             };
    
  • russh/src/keys/format/pkcs8_legacy.rs+5 4 modified
    @@ -1,11 +1,12 @@
     use std::borrow::Cow;
     use std::convert::TryFrom;
     
    -use aes::cipher::{BlockDecryptMut, KeyIvInit};
    +use aes::cipher::KeyIvInit;
     use aes::*;
     use block_padding::Pkcs7;
    -use ssh_key::private::{Ed25519Keypair, Ed25519PrivateKey, KeypairData};
    +use cipher::BlockModeDecrypt;
     use ssh_key::PrivateKey;
    +use ssh_key::private::{Ed25519Keypair, Ed25519PrivateKey, KeypairData};
     use yasna::BERReaderSeq;
     
     use super::Encryption;
    @@ -139,13 +140,13 @@ impl Encryption {
                     #[allow(clippy::unwrap_used)] // parameters are static
                     let c = cbc::Decryptor::<Aes128>::new_from_slices(key, iv).unwrap();
                     let mut dec = ciphertext.to_vec();
    -                Ok(c.decrypt_padded_mut::<Pkcs7>(&mut dec)?.into())
    +                Ok(c.decrypt_padded::<Pkcs7>(&mut dec)?.into())
                 }
                 Encryption::Aes256Cbc(ref iv) => {
                     #[allow(clippy::unwrap_used)] // parameters are static
                     let c = cbc::Decryptor::<Aes256>::new_from_slices(key, iv).unwrap();
                     let mut dec = ciphertext.to_vec();
    -                Ok(c.decrypt_padded_mut::<Pkcs7>(&mut dec)?.into())
    +                Ok(c.decrypt_padded::<Pkcs7>(&mut dec)?.into())
                 }
             }
         }
    
  • russh/src/keys/known_hosts.rs+1 1 modified
    @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
     use std::env;
     
     use data_encoding::BASE64_MIME;
    -use hmac::{Hmac, Mac};
    +use hmac::{Hmac, KeyInit, Mac};
     use log::debug;
     use sha1::Sha1;
     
    
  • russh/src/keys/mod.rs+3 4 modified
    @@ -61,7 +61,6 @@ use std::io::Read;
     use std::path::Path;
     use std::string::FromUtf8Error;
     
    -use aes::cipher::block_padding::UnpadError;
     use aes::cipher::inout::PadError;
     use data_encoding::BASE64_MIME;
     use thiserror::Error;
    @@ -145,7 +144,7 @@ pub enum Error {
         Pad(#[from] PadError),
     
         #[error(transparent)]
    -    Unpad(#[from] UnpadError),
    +    Unpad(#[from] aes::cipher::block_padding::Error),
     
         #[error("Base64 decoding error: {0}")]
         Decode(#[from] data_encoding::DecodeError),
    @@ -1251,7 +1250,7 @@ Cog3JMeTrb3LiPHgN6gU2P30MRp6L1j1J/MtlOAr5rux
         #[cfg(unix)]
         async fn test_sign_request_cert_missing_key_returns_agent_failure() {
             use crate::keys::agent::client::AgentClient;
    -        
    +
             env_logger::try_init().unwrap_or(());
     
             let (mut agent, agent_path, _dir) = spawn_agent().await.unwrap();
    @@ -1303,7 +1302,7 @@ Cog3JMeTrb3LiPHgN6gU2P30MRp6L1j1J/MtlOAr5rux
         #[cfg(unix)]
         async fn test_sign_request_missing_key_returns_agent_failure() {
             use crate::keys::agent::client::AgentClient;
    -        
    +
             env_logger::try_init().unwrap_or(());
     
             let (mut agent, agent_path, _dir) = spawn_agent().await.unwrap();
    
  • russh/src/mac/crypto.rs+1 1 modified
    @@ -47,7 +47,7 @@ where
     
         fn compute(&self, sequence_number: u32, payload: &[u8], output: &mut [u8]) {
             #[allow(clippy::unwrap_used)]
    -        let mut hmac = <M as digest::Mac>::new_from_slice(&self.key).unwrap();
    +        let mut hmac = <M as KeyInit>::new_from_slice(&self.key).unwrap();
             let mut seqno_buf = [0; 4];
             BigEndian::write_u32(&mut seqno_buf, sequence_number);
             hmac.update(&seqno_buf);
    
  • russh/src/negotiation.rs+49 65 modified
    @@ -191,18 +191,14 @@ impl Default for Preferred {
         }
     }
     
    -pub(crate) fn parse_kex_algo_list(list: &str) -> Vec<&str> {
    -    list.split(',').collect()
    -}
    -
     pub(crate) trait Select {
         fn is_server() -> bool;
     
    -    fn select<S: AsRef<str> + Clone>(
    -        a: &[S],
    -        b: &[&str],
    +    fn select<A: AsRef<str> + Clone, B: AsRef<str> + Clone>(
    +        a: &[A],
    +        b: &[B],
             kind: AlgorithmKind,
    -    ) -> Result<(bool, S), Error>;
    +    ) -> Result<(bool, A), Error>;
     
         /// `available_host_keys`, if present, is used to limit the host key algorithms to the ones we have keys for.
         fn read_kex(
    @@ -217,18 +213,18 @@ pub(crate) trait Select {
     
             // Key exchange
     
    -        let kex_string = String::decode(&mut r)?;
    +        let kex_list = NameList::decode(&mut r)?;
             // Filter out extension kex names from both lists before selecting
             let _local_kexes_no_ext = pref
                 .kex
                 .iter()
                 .filter(|k| !KEX_EXTENSION_NAMES.contains(k))
                 .cloned()
                 .collect::<Vec<_>>();
    -        let _remote_kexes_no_ext = parse_kex_algo_list(&kex_string)
    -            .into_iter()
    +        let _remote_kexes_no_ext = kex_list
    +            .iter()
                 .filter(|k| {
    -                kex::Name::try_from(*k)
    +                kex::Name::try_from(k.as_str())
                         .ok()
                         .map(|k| !KEX_EXTENSION_NAMES.contains(&k))
                         .unwrap_or(false)
    @@ -253,7 +249,7 @@ pub(crate) trait Select {
                 } else {
                     EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER
                 }],
    -            &parse_kex_algo_list(&kex_string),
    +            &kex_list,
                 AlgorithmKind::Kex,
             )
             .is_ok();
    @@ -264,68 +260,56 @@ pub(crate) trait Select {
     
             // Host key
     
    -        let key_string = String::decode(&mut r)?;
    +        let key_list = NameList::decode(&mut r)?;
             let possible_host_key_algos = match available_host_keys {
                 Some(available_host_keys) => pref.possible_host_key_algos_for_keys(available_host_keys),
                 None => pref.key.iter().map(ToOwned::to_owned).collect::<Vec<_>>(),
             };
     
    -        let (key_both_first, key_algorithm) = Self::select(
    -            &possible_host_key_algos[..],
    -            &parse_kex_algo_list(&key_string),
    -            AlgorithmKind::Key,
    -        )?;
    +        let (key_both_first, key_algorithm) =
    +            Self::select(&possible_host_key_algos[..], &key_list, AlgorithmKind::Key)?;
     
             // Cipher
     
    -        let cipher_string = String::decode(&mut r)?;
    -        let (_cipher_both_first, cipher) = Self::select(
    -            &pref.cipher,
    -            &parse_kex_algo_list(&cipher_string),
    -            AlgorithmKind::Cipher,
    -        )?;
    +        let cipher_list = NameList::decode(&mut r)?;
    +        let (_cipher_both_first, cipher) =
    +            Self::select(&pref.cipher, &cipher_list, AlgorithmKind::Cipher)?;
             String::decode(&mut r)?; // cipher server-to-client.
     
             // MAC
     
             let need_mac = CIPHERS.get(&cipher).map(|x| x.needs_mac()).unwrap_or(false);
     
    -        let client_mac = match Self::select(
    -            &pref.mac,
    -            &parse_kex_algo_list(&String::decode(&mut r)?),
    -            AlgorithmKind::Mac,
    -        ) {
    -            Ok((_, m)) => m,
    -            Err(e) => {
    -                if need_mac {
    -                    return Err(e);
    -                } else {
    -                    mac::NONE
    +        let client_mac =
    +            match Self::select(&pref.mac, &NameList::decode(&mut r)?, AlgorithmKind::Mac) {
    +                Ok((_, m)) => m,
    +                Err(e) => {
    +                    if need_mac {
    +                        return Err(e);
    +                    } else {
    +                        mac::NONE
    +                    }
                     }
    -            }
    -        };
    -        let server_mac = match Self::select(
    -            &pref.mac,
    -            &parse_kex_algo_list(&String::decode(&mut r)?),
    -            AlgorithmKind::Mac,
    -        ) {
    -            Ok((_, m)) => m,
    -            Err(e) => {
    -                if need_mac {
    -                    return Err(e);
    -                } else {
    -                    mac::NONE
    +            };
    +        let server_mac =
    +            match Self::select(&pref.mac, &NameList::decode(&mut r)?, AlgorithmKind::Mac) {
    +                Ok((_, m)) => m,
    +                Err(e) => {
    +                    if need_mac {
    +                        return Err(e);
    +                    } else {
    +                        mac::NONE
    +                    }
                     }
    -            }
    -        };
    +            };
     
             // Compression
     
             // client-to-server compression.
             let client_compression = compression::Compression::new(
                 &Self::select(
                     &pref.compression,
    -                &parse_kex_algo_list(&String::decode(&mut r)?),
    +                &NameList::decode(&mut r)?,
                     AlgorithmKind::Compression,
                 )?
                 .1,
    @@ -335,7 +319,7 @@ pub(crate) trait Select {
             let server_compression = compression::Compression::new(
                 &Self::select(
                     &pref.compression,
    -                &parse_kex_algo_list(&String::decode(&mut r)?),
    +                &NameList::decode(&mut r)?,
                     AlgorithmKind::Compression,
                 )?
                 .1,
    @@ -369,15 +353,15 @@ impl Select for Server {
             true
         }
     
    -    fn select<S: AsRef<str> + Clone>(
    -        server_list: &[S],
    -        client_list: &[&str],
    +    fn select<A: AsRef<str> + Clone, B: AsRef<str> + Clone>(
    +        server_list: &[A],
    +        client_list: &[B],
             kind: AlgorithmKind,
    -    ) -> Result<(bool, S), Error> {
    +    ) -> Result<(bool, A), Error> {
             let mut both_first_choice = true;
             for c in client_list {
                 for s in server_list {
    -                if c == &s.as_ref() {
    +                if c.as_ref() == s.as_ref() {
                         return Ok((both_first_choice, s.clone()));
                     }
                     both_first_choice = false
    @@ -386,7 +370,7 @@ impl Select for Server {
             Err(Error::NoCommonAlgo {
                 kind,
                 ours: server_list.iter().map(|x| x.as_ref().to_owned()).collect(),
    -            theirs: client_list.iter().map(|x| (*x).to_owned()).collect(),
    +            theirs: client_list.iter().map(|x| x.as_ref().to_owned()).collect(),
             })
         }
     }
    @@ -396,15 +380,15 @@ impl Select for Client {
             false
         }
     
    -    fn select<S: AsRef<str> + Clone>(
    -        client_list: &[S],
    -        server_list: &[&str],
    +    fn select<A: AsRef<str> + Clone, B: AsRef<str> + Clone>(
    +        client_list: &[A],
    +        server_list: &[B],
             kind: AlgorithmKind,
    -    ) -> Result<(bool, S), Error> {
    +    ) -> Result<(bool, A), Error> {
             let mut both_first_choice = true;
             for c in client_list {
                 for s in server_list {
    -                if s == &c.as_ref() {
    +                if s.as_ref() == c.as_ref() {
                         return Ok((both_first_choice, c.clone()));
                     }
                     both_first_choice = false
    @@ -413,7 +397,7 @@ impl Select for Client {
             Err(Error::NoCommonAlgo {
                 kind,
                 ours: client_list.iter().map(|x| x.as_ref().to_owned()).collect(),
    -            theirs: server_list.iter().map(|x| (*x).to_owned()).collect(),
    +            theirs: server_list.iter().map(|x| x.as_ref().to_owned()).collect(),
             })
         }
     }
    
  • russh/src/server/encrypted.rs+30 11 modified
    @@ -217,7 +217,10 @@ mod tests {
             }))
             .await;
     
    -        assert_rejected(result, "server accepted an exec request with trailing bytes");
    +        assert_rejected(
    +            result,
    +            "server accepted an exec request with trailing bytes",
    +        );
         }
     
         #[tokio::test]
    @@ -230,7 +233,10 @@ mod tests {
             }))
             .await;
     
    -        assert_rejected(result, "server accepted a signal request with trailing bytes");
    +        assert_rejected(
    +            result,
    +            "server accepted a signal request with trailing bytes",
    +        );
         }
     
         #[tokio::test]
    @@ -242,7 +248,10 @@ mod tests {
             }))
             .await;
     
    -        assert_rejected(result, "server accepted a service request with trailing bytes");
    +        assert_rejected(
    +            result,
    +            "server accepted a service request with trailing bytes",
    +        );
         }
     
         #[tokio::test]
    @@ -256,7 +265,10 @@ mod tests {
             }))
             .await;
     
    -        assert_rejected(result, "server accepted a none auth request with trailing bytes");
    +        assert_rejected(
    +            result,
    +            "server accepted a none auth request with trailing bytes",
    +        );
         }
     
         #[tokio::test]
    @@ -508,7 +520,12 @@ impl Encrypted {
                         PublicKeyOrCertificate::Certificate(ref cert) => {
                             // Validate certificate expiration
                             let now = SystemTime::now();
    -                        if now < cert.valid_after_time() || now > cert.valid_before_time() {
    +                        if cert.valid_after_time().map(|t| now < t).unwrap_or_default()
    +                            || cert
    +                                .valid_before_time()
    +                                .map(|t| now > t)
    +                                .unwrap_or_default()
    +                        {
                                 warn!("Certificate is expired or not yet valid");
                                 reject_auth_request(until, &mut self.write, auth_request).await?;
                                 return Ok(());
    @@ -840,11 +857,9 @@ impl Session {
                         handler.extended_data(channel_num, ext, &data, self).await
                     } else {
                         if let Some(chan) = self.channels.get(&channel_num) {
    -                        chan.send(ChannelMsg::Data {
    -                            data: data.clone(),
    -                        })
    -                        .await
    -                        .unwrap_or(())
    +                        chan.send(ChannelMsg::Data { data: data.clone() })
    +                            .await
    +                            .unwrap_or(())
                         }
                         handler.data(channel_num, &data, self).await
                     }
    @@ -966,7 +981,11 @@ impl Session {
                                         info!("pty-req: unknown pty code {code:?}");
                                     }
                                     i += 1;
    -                                mode_bytes = &mode_bytes[5..];
    +
    +                                #[allow(clippy::indexing_slicing, reason = "length checked")]
    +                                {
    +                                    mode_bytes = &mode_bytes[5..];
    +                                }
                                 }
                             }
                             map_err!(ensure_end(r))?;
    
  • russh/src/server/kex.rs+7 6 modified
    @@ -10,9 +10,9 @@ use ssh_key::Algorithm;
     use super::*;
     use crate::helpers::sign_with_hash_alg;
     use crate::kex::dh::biguint_to_mpint;
    -use crate::kex::{KexAlgorithm, KexAlgorithmImplementor, KexCause, KEXES};
    +use crate::kex::{KEXES, KexAlgorithm, KexAlgorithmImplementor, KexCause};
     use crate::keys::key::PrivateKeyWithHashAlg;
    -use crate::negotiation::{is_key_compatible_with_algo, Names, Select};
    +use crate::negotiation::{Names, Select, is_key_compatible_with_algo};
     use crate::parsing::ensure_end;
     use crate::{msg, negotiation};
     
    @@ -180,7 +180,9 @@ impl ServerKex {
                     debug!("client requests a gex group: {gex_params:?}");
     
                     let Some(dh_group) = handler.lookup_dh_gex_group(&gex_params).await? else {
    -                    debug!("server::Handler impl did not find a matching DH group (is lookup_dh_gex_group implemented?)");
    +                    debug!(
    +                        "server::Handler impl did not find a matching DH group (is lookup_dh_gex_group implemented?)"
    +                    );
                         return Err(Error::Kex)?;
                     };
     
    @@ -328,6 +330,7 @@ impl ServerKex {
                         );
                         return Err(Error::Kex.into());
                     }
    +                #[allow(clippy::indexing_slicing, reason = "checked")]
                     let r = &input.buffer[1..];
                     ensure_end(&r)?;
     
    @@ -381,9 +384,7 @@ fn compute_keys(
     
     #[cfg(test)]
     mod tests {
    -    use crate::tests::raw_no_crypto::{
    -        assert_rejected, kexinit_payload, raw_kex_signal, timeout,
    -    };
    +    use crate::tests::raw_no_crypto::{assert_rejected, kexinit_payload, raw_kex_signal, timeout};
     
         #[tokio::test]
         async fn kexinit_with_trailing_bytes_rejected_by_server() {
    
  • russh/src/server/session.rs+11 11 modified
    @@ -5,15 +5,14 @@ use std::sync::Arc;
     use channels::WindowSizeRef;
     use kex::ServerKex;
     use log::debug;
    -use negotiation::parse_kex_algo_list;
     use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
    -use tokio::sync::mpsc::{channel, Receiver, Sender};
    +use tokio::sync::mpsc::{Receiver, Sender, channel};
     use tokio::sync::oneshot;
     
     use super::*;
     use crate::channels::{Channel, ChannelMsg, ChannelReadHalf, ChannelRef, ChannelWriteHalf};
     use crate::helpers::NameList;
    -use crate::kex::{KexCause, SessionKexState, EXTENSION_SUPPORT_AS_CLIENT};
    +use crate::kex::{EXTENSION_SUPPORT_AS_CLIENT, KexCause, SessionKexState};
     use crate::{map_err, msg};
     
     /// A connected server session. This type is unique to a client.
    @@ -101,7 +100,11 @@ pub struct Handle {
     
     impl Handle {
         /// Send data to the session referenced by this handler.
    -    pub async fn data(&self, id: ChannelId, data: impl Into<bytes::Bytes>) -> Result<(), bytes::Bytes> {
    +    pub async fn data(
    +        &self,
    +        id: ChannelId,
    +        data: impl Into<bytes::Bytes>,
    +    ) -> Result<(), bytes::Bytes> {
             let data = data.into();
             self.sender
                 .send(Msg::Channel(id, ChannelMsg::Data { data }))
    @@ -121,10 +124,7 @@ impl Handle {
         ) -> Result<(), bytes::Bytes> {
             let data = data.into();
             self.sender
    -            .send(Msg::Channel(id, ChannelMsg::ExtendedData {
    -                ext,
    -                data,
    -            }))
    +            .send(Msg::Channel(id, ChannelMsg::ExtendedData { ext, data }))
                 .await
                 .map_err(|e| match e.0 {
                     Msg::Channel(_, ChannelMsg::ExtendedData { data, .. }) => data,
    @@ -401,7 +401,7 @@ impl Handle {
                         });
                     }
                     Some(ChannelMsg::OpenFailure(reason)) => {
    -                    return Err(Error::ChannelOpenFailure(reason))
    +                    return Err(Error::ChannelOpenFailure(reason));
                     }
                     None => {
                         return Err(Error::Disconnect);
    @@ -1261,11 +1261,11 @@ impl Session {
                     let Some(mut r) = e.client_kex_init.get(17..) else {
                         return Ok(());
                     };
    -                if let Ok(kex_string) = String::decode(&mut r) {
    +                if let Ok(kex_list) = NameList::decode(&mut r) {
                         use super::negotiation::Select;
                         key_extension_client = super::negotiation::Server::select(
                             &[EXTENSION_SUPPORT_AS_CLIENT],
    -                        &parse_kex_algo_list(&kex_string),
    +                        &kex_list,
                             AlgorithmKind::Kex,
                         )
                         .is_ok();
    

Vulnerability mechanics

Root cause

"The server-side SSH identification reader did not enforce protocol rules for client pre-banner lines and did not limit their count."

Attack vector

A remote peer can send malformed identification input to a server built on russh. This input can include preliminary banner lines before the actual SSH identification string. The server-side reader, using the same permissive path as the client, accepts these lines instead of rejecting them early. This allows the attacker to hold connection setup resources in the cleartext pre-authentication phase until an application-level timeout or external resource limit is reached [ref_id=1].

Affected code

The vulnerability existed in the identification reader logic located in `russh/src/ssh_read.rs` and `russh/src/server/mod.rs`. The same `read_ssh_id()` function was used for both client and server contexts, leading to permissive parsing on the server side.

What the fix does

The patch separates the server-side and client-side identification reading behaviors. The server path now strictly rejects client pre-banner lines, aligning with RFC 4253. Additionally, explicit limits for line length and the number of pre-banner lines have been introduced to prevent resource exhaustion and enforce protocol compliance [patch_id=5531338, patch_id=5531339].

Preconditions

  • authNo authentication is required.
  • networkThe vulnerable server is reachable over the network.
  • inputThe attacker must send malformed identification input, including pre-banner lines, before the SSH identification string.

Reproduction

# PoC Inline highest-CVSS PoC: unauthenticated remote client pre-banner input to the server identification parser. This demonstrates AV:N/AC:L/PR:N/UI:N.

```rust #[tokio::test] async fn poc_server_accepts_client_pre_banner_before_ssh_id() { use russh::server; use tokio::io::{AsyncReadExt, AsyncWriteExt};

let config = std::sync::Arc::new(server::Config::default()); let (mut client, server_stream) = tokio::io::duplex(4096);

let server = tokio::spawn(async move { server::run_stream(config, server_stream, NoAuthHandler).await });

let mut server_id = Vec::new(); client.read_until(b'\n', &mut server_id).await.unwrap();

client .write_all(b"attacker-controlled pre-banner\r\nSSH-2.0-poc\r\n") .await .unwrap();

let result = tokio::time::timeout(std::time::Duration::from_millis(250), server).await;

assert!( result.is_err(), "vulnerable code keeps processing after accepting a client pre-banner before SSH identification" ); } ``` On vulnerable code, the server-side reader accepts the client pre-banner line and continues instead of rejecting the malformed identification input promptly. The fixed parser rejects client pre-banner lines on the server path. [ref_id=1]

Generated on Jun 10, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

1

News mentions

0

No linked articles in our index yet.