# Russh SSH message fields were decoded through allocation-first parsers before field-specific bounds

russh decoded attacker-controlled SSH name-lists and strings into owned allocations before bounds checks, letting an unauthenticated peer drive multi-GiB memory growth pre-auth.

- Payout: Research
- Severity: high
- Program: russh
- Found by: mjc
- Disclosed: 2026-06-11
- CVE: CVE-2026-48110
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-4r3c-5hpg-58qr
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-4r3c-5hpg-58qr
- Tags: area:devices domain:network impact:dos kind:research platform:ghsa sev:high tech:rust technique:chained technique:source-review vuln:dos vuln:memory-corruption

**Target:** SSH transport/encrypted-message parser (KEXINIT, USERAUTH, SERVICE_REQUEST, channel requests)
**Root cause:** Handlers used ssh_encoding::Decode into owned String/Bytes/Vec/NameList, accepting attacker-controlled length prefixes and allocating or splitting name-lists before checking field-specific bounds or packet truncation.
**Steps:**
1. Build a KEXINIT payload: `SSH_MSG_KEXINIT` + 16-byte cookie + first name-list string of length ~262103 containing `none,` followed by `a,` repeated 131000 times + `a`
2. Encode remaining algorithm name-lists (`ssh-ed25519`, `none` x5, empty language lists) and trailing `0` + 4 zero bytes
3. Feed payload to `Server::read_kex(&payload, &preferred, None, &kex::KexCause::Initial)` from 512 concurrent threads, 8 iterations each
4. Observe RSS grow from ~4 MiB to ~4.45 GiB with zero parser errors
5. Regression form: name-list length prefix `1_048_575` with absent body returns `Err(SshEncoding(Length))` instead of `Err(Error::PacketSize(_))`
**How found:** Code review of the SSH transport and encrypted-message parsers revealed a repeated pattern of decoding into owned types via ssh_encoding::Decode and validating semantics afterward; comparing against OpenSSH's bounded sshbuf helpers showed the missing early bounds check, and a direct-parser stress harness quantified the allocation fanout.
**Exploitation:** Unauthenticated remote client sends concurrent packet-sized KEXINIT messages with a large high-fanout name-list, reaching the pre-auth server-side key-exchange parser and forcing allocation-heavy owned decoding and name-list splitting; at scale this exhausts memory. Historical pre-0.58.0: negotiate RFC zlib, complete first KEX, then send a ~652-byte compressed rekey KEXINIT that inflates to ~600KB and grows CryptoVec decompression buffers before hitting the same parser, aborting the process on allocator failure under address-space limits.
**Escalation:** Historical releases combine zlib compression + rekey with CryptoVec decompression growth, so ~1 KiB on the wire amplifies into large buffers feeding the unbounded name-list parser.
**Insight:** In length-prefixed protocol parsers, check whether the decoder allocates from the wire length prefix before verifying packet completion or local field bounds; a truncated-packet test that returns a decode/length error instead of a packet-size error is a tell that allocation happened first. Name-lists with many tiny comma-separated entries are a cheap fanout amplifier.

## Proof of concept (copied exactly from the original report)

**KEXINIT payload with 131k-name list**

```
let payload = Arc::new(kexinit_with_kex_list(
        &("none,".to_owned() + &"a,".repeat(131_000) + "a"),
    ));
    let preferred = Arc::new(no_crypto_preferred());
    let barrier = Arc::new(std::sync::Barrier::new(THREADS + 1));
```

**SERVICE_REQUEST with oversized length prefix**

```
The `SERVICE_REQUEST` variant was checked after key exchange but before user authentication. It has the same allocation-first shape with a tiny packet containing only `SSH_MSG_SERVICE_REQUEST` plus a `1_048_575` length prefix, and the vulnerable code returns `Err(SshEncoding(Length))` rather than `PacketSize`.
```

