{"id":"GHSA-wwx6-x28x-8259","summary":"russh: Post-decompression SSH packet size was not bounded, allowing remote oversized compressed packets","details":"### Summary\n\nWhen SSH compression is enabled, `russh` accepted compressed packets whose on-wire size passed the normal transport packet-length checks but whose decompressed size was much larger. This allowed a remote peer to send oversized post-decompression packets that should have been rejected.\n\nIn current releases, this is a remote denial-of-service / resource-exhaustion issue in the post-decompression receive path.\n\nIn older releases before `0.58.0`, the same remote decompression path used `CryptoVec`, which appears to make the historical impact worse.\n\n### Details\n\nThe normal SSH transport read path enforces a packet-length limit before the packet body is read:\n\n- `russh/src/cipher/mod.rs`\n\nHowever, RFC 4253 compression is applied to the SSH `payload` field only. The `packet_length` field and MAC are computed over the compressed payload, so a packet that is reasonably sized on the wire can still expand to a much larger message body after decompression.\n\nIn `russh`, compressed packet bodies are later decompressed in:\n\n- `russh/src/compression.rs`\n- `russh/src/client/mod.rs`\n- `russh/src/server/session.rs`\n\nBefore the fix, `Decompress::decompress()` grew its output buffer by repeated doubling and did not enforce a separate post-decompression ceiling. That meant a peer could send a small compressed packet that passed the normal on-wire transport length checks and then inflate it into a much larger packet after decompression.\n\nIt was verified that an attacker-crafted compressed payload can stay below the normal `256 KiB` implementation transport packet cap while still inflating above the intended post-decompression bound. In other words, this is not only a \"large on-wire packet\" issue.\n\nVersion detail:\n\n- The underlying post-decompression bounds bug appears to affect `russh` as far back as `0.34.0`.\n- In historical releases `\u003e= 0.34.0, \u003c 0.58.0`, the remote decompression path still used `CryptoVec`. Remote compressed SSH traffic could drive that path, and under constrained memory that historical code path could abort the process.\n- In current-style releases `\u003e= 0.58.0`, non-secret packet/decompression buffers were moved off `CryptoVec` and onto `Vec\u003cu8\u003e`, but the post-decompression size still remained unbounded. So the bug class remained reachable remotely, but the maintained-line impact is a current remote DoS / oversized-packet-acceptance issue rather than the older `CryptoVec`-based abort story.\n- The maintained-line fix was verified against `0.60.2`.\n\nCompression is not selected in a default-vs-default `russh` session because the default preference order puts `none` first. However, the default server configuration still advertises `zlib` and `zlib@openssh.com`, and server-side negotiation follows the client's preference order for common algorithms. A client that prefers compression can therefore negotiate it with a default `russh` server.\n\nOpenSSH portable was checked at `/home/mjc/projects/openssh-portable` commit `45b30e0a5`. OpenSSH enforces a `256 KiB` transport packet cap before decompression, but it does not reuse that cap after decompression. Instead, decompression writes to an `sshbuf`, which is indirectly bounded by OpenSSH's `SSHBUF_SIZE_MAX` hard maximum of `0x8000000` bytes (`128 MiB`).\n\nThe patch direction should follow that model: add an explicit post-decompression ceiling of `128 MiB`, rather than assuming the compressed transport packet cap also bounds decompressed payload size.\n\nRelevant OpenSSH reference points:\n\n- `/home/mjc/projects/openssh-portable/packet.c`: `PACKET_MAX_SIZE (256 * 1024)`\n- `/home/mjc/projects/openssh-portable/packet.c`: `uncompress_buffer()` inflates into `compression_buffer`\n- `/home/mjc/projects/openssh-portable/sshbuf.h`: `SSHBUF_SIZE_MAX 0x8000000`\n\n### RFC / OpenSSH Comparison\n\nRFC 4253 section 6 defines the binary packet format:\n\n- `packet_length`\n- `padding_length`\n- `payload`\n- random padding\n- MAC\n\nRFC 4253 section 6.2 says that, when compression is negotiated, the `payload` field is compressed, and that `packet_length` and MAC are computed from the compressed payload. The RFC also says implementations should check that packet length is reasonable to avoid denial-of-service and buffer-overflow attacks.\n\nThat means the pre-decompression transport packet length check is necessary but not sufficient. A correct implementation still needs a reasonable bound on the decompressed payload that becomes parser input.\n\nOpenSSH provides such a bound indirectly through `sshbuf`'s hard maximum. The `russh` fix should make the corresponding post-decompression bound explicit.\n\n### PoC\n\nThere were two kinds of proof:\n\n- a wire-cap sanity test showing an attacker-crafted best-compressed `DEBUG` payload can stay below the normal SSH transport packet cap while still inflating beyond the intended post-decompression bound\n- direct client and server receive-path tests that exercise the oversized post-decompression behavior itself\n\nThe current in-tree regression tests are:\n\n- `tests::compress::oversized_debug_payload_can_stay_below_wire_cap`\n- `compression::tests::oversized_decompressed_packet_is_rejected`\n- `client::tests::compressed_debug_is_ignored_after_client_parses_it`\n- `client::tests::oversized_compressed_debug_is_rejected_before_client_ignores_it`\n- `server::session::tests::compressed_debug_is_ignored_after_server_parses_it`\n- `server::session::tests::oversized_compressed_debug_is_rejected_before_server_ignores_it`\n\nThe important behavior is:\n\n1. An attacker-crafted best-compressed `DEBUG` payload can stay below the normal `256 KiB` transport packet cap while still inflating beyond `128 MiB`.\n2. In the direct client and server receive paths, small compressed `DEBUG` packets are still ignored normally after parsing.\n3. In the direct client and server receive paths, oversized compressed `DEBUG` packets are rejected before the implementation reaches the normal \"ignore DEBUG\" behavior.\n\nThe strongest PoC for severity is the unauthenticated server-side case. A malicious client can choose `zlib` in the initial key exchange, because the default server advertises it and server-side negotiation follows the client's preference order for common algorithms. After `NEWKEYS`, but before authentication, the client can send a transport-layer `SSH_MSG_DEBUG` packet whose compressed body is below the transport packet cap but whose decompressed body exceeds the post-decompression cap.\n\nThat demonstrates the `AV:N/AC:L/PR:N/UI:N` case directly: the attacker is a remote SSH client and does not need a successfully authenticated session.\n\n```rust\nfn compressed_debug_payload(payload_len: usize) -\u003e Vec\u003cu8\u003e {\n    let mut payload = vec![b'A'; payload_len];\n    payload[0] = crate::msg::DEBUG;\n\n    let mut encoder =\n        flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());\n    encoder.write_all(&payload).unwrap();\n    let compressed = encoder.finish().unwrap();\n\n    assert!(\n        compressed.len() \u003c 256 * 1024,\n        \"oversized post-decompression payload still fits under the wire cap\"\n    );\n    compressed\n}\n\nfn incoming_packet(compressed: Vec\u003cu8\u003e) -\u003e SSHBuffer {\n    let mut buffer = SSHBuffer::new();\n    // maybe_decompress() receives the clear SSHBuffer after packet framing,\n    // and decompresses bytes after packet_length + padding_length.\n    buffer.buffer.extend_from_slice(&[0; 5]);\n    buffer.buffer.extend_from_slice(&compressed);\n    buffer\n}\n\n#[test]\nfn unauthenticated_client_zlib_debug_is_rejected_by_server_before_auth() {\n    let mut server = preauth_server_session_after_newkeys_with_zlib_decompressor();\n    let oversized = MAXIMUM_DECOMPRESSED_PACKET_LEN + 1024;\n    let buffer = incoming_packet(compressed_debug_payload(oversized));\n\n    let err = server.maybe_decompress(&buffer).unwrap_err();\n    assert!(\n        matches!(err, crate::Error::PacketSize(len) if len \u003e MAXIMUM_DECOMPRESSED_PACKET_LEN)\n    );\n}\n```\n\nThe equivalent wire-level attack shape is:\n\n```text\n1. Connect to a russh server using the default compression advertisement.\n2. Send SSH_MSG_KEXINIT with compression client-to-server preference:\n   zlib,zlib@openssh.com,none\n3. Complete key exchange and send SSH_MSG_NEWKEYS.\n4. Before any SSH_MSG_USERAUTH_REQUEST, send a compressed SSH_MSG_DEBUG packet:\n   - compressed packet body: \u003c 256 KiB\n   - decompressed packet body: \u003e 128 MiB\n5. Vulnerable behavior: russh accepts and inflates the packet, then reaches the\n   normal DEBUG ignore path.\n6. Fixed behavior: russh rejects during decompression with Error::PacketSize.\n```\n\nThe direct receive-path client/server regression tests are still useful because they isolate the bug precisely. They construct the post-decryption compressed packet body passed to `maybe_decompress()` and prove that the oversized packet is rejected before normal `DEBUG` ignore handling. The server-side pre-auth variant above is the one that justifies the highest CVSS framing for this bug.\n\nThe most important targeted checks are:\n\n```bash\ncargo test -p russh oversized_debug_payload_can_stay_below_wire_cap -- --nocapture\ncargo test -p russh oversized_compressed_debug_is_rejected_before_client_ignores_it -- --nocapture\ncargo test -p russh oversized_compressed_debug_is_rejected_before_server_ignores_it -- --nocapture\n```\n\nBefore the fix, both the direct client and direct server receive-path oversized checks went red because the compressed payload was accepted and decompressed instead of being rejected at the post-decompression boundary. After the fix, they pass.\n\n### Impact\n\nSuggested CVSS v3.1 for current maintained releases:\n\n- `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H`\n- Score: `7.5`\n\nReasoning:\n\n- `AV:N`: reachable by a remote SSH peer\n- `AC:L`: straightforward once compression is enabled\n- `PR:N`, `UI:N`: no prior auth or user interaction required\n- `C:N`, `I:N`: confidentiality or integrity impact was not demonstrated\n- `A:H`: remote peer can cause oversized post-decompression packet processing and disconnect / denial of service\n\nAffected versions:\n\n- historical stronger case: `russh \u003e= 0.34.0, \u003c 0.58.0`\n- current maintained remote DoS case: `russh \u003e= 0.58.0`, including `0.60.3`\n\n### Fix / Patch Direction\n\nAdd an explicit maximum decompressed SSH packet size and enforce it inside `Decompress::decompress()` before returning decompressed bytes to the client or server packet parser.\n\nThe intended ceiling is `128 MiB`, matching OpenSSH portable's effective `sshbuf` hard maximum for post-decompression packet storage. The fix should reject decompression output larger than that bound with a packet-size error before normal message dispatch.\n\nThe fix should preserve normal compressed packet behavior below the cap, including `DEBUG` packets that are decompressed and then ignored through the existing normal path.\n\nPatch branch:\n\n```text\nfix/zlib-decompression-cap\n```","aliases":["CVE-2026-46702"],"modified":"2026-06-11T14:15:12.102086387Z","published":"2026-05-29T19:37:14Z","related":["CGA-rgpm-f3gp-3747"],"database_specific":{"cwe_ids":["CWE-770"],"severity":"HIGH","nvd_published_at":"2026-06-10T22:17:00Z","github_reviewed_at":"2026-05-29T19:37:14Z","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/Eugeny/russh/security/advisories/GHSA-wwx6-x28x-8259"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-46702"},{"type":"PACKAGE","url":"https://github.com/Eugeny/russh"}],"affected":[{"package":{"name":"russh","ecosystem":"crates.io","purl":"pkg:cargo/russh"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.34.0"},{"fixed":"0.61.1"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-wwx6-x28x-8259/GHSA-wwx6-x28x-8259.json"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"}]}