Update Rust crate russh to 0.61 [SECURITY]#191
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
0.58→0.61russh has pre-auth DoS via unbounded allocation in its keyboard-interactive auth handler
CVE-2026-42189 / GHSA-f5v4-2wr6-hqmg
More information
Details
Summary
A pre-authentication denial-of-service vulnerability exists in the server's keyboard-interactive authentication handler. A malicious client can crash any russh-based server that implements keyboard-interactive auth (e.g., for 2FA/TOTP) with a single malformed packet, requiring no credentials.
Vulnerability Details
In
russh/src/server/encrypted.rs, the functionread_userauth_info_responsedecodes au32count from the client'sSSH_MSG_USERAUTH_INFO_RESPONSEand passes it directly toVec::with_capacity():An attacker can send
n = 0x10000000(268M) or larger in a minimal packet (~50 bytes after encryption). The server attempts to allocaten * ~24 bytes(size ofOption<Bytes>) = ~6.4GB, causing an OOM crash.Attack Flow
USERAUTH_REQUESTwith methodkeyboard-interactiveAuth::Partialwith prompts (standard for 2FA/TOTP)USERAUTH_INFO_RESPONSEwithn = 0x10000000and no response dataVec::with_capacity(268_435_456), OOM killedNo authentication is required. The allocation occurs before the handler validates any credentials. The attack is repeatable faster than the server can restart.
Affected Configurations
Any russh-based server where the
Handler::auth_keyboard_interactiveimplementation returnsAuth::Partial(i.e., sends prompts to the client). The default handler returnsAuth::reject()and is not affected.Source code review suggests that downstream projects using keyboard-interactive for multi-step auth (e.g., TOTP/2FA) follow the affected pattern, since returning
Auth::Partialbefore credential verification is the intended API usage for prompting.Confirmed End-to-End PoC
There is a complete Docker-contained PoC confirming the OOM kill:
Auth::Partialfor keyboard-interactiveUSERAUTH_INFO_RESPONSEAvailable on request.
Proposed Fix
Cap the
Vec::with_capacityallocation to what the remaining packet data can actually contain. Each response requires at least 4 bytes (length prefix), so:This bounds the allocation to at most the packet size (~256KB), while preserving the existing behavior for well-formed packets. This fix has been implemented, tested, and contributed via the temporary private fork.
Severity
Pre-auth, remote, no credentials required, crashes the server process affecting all active sessions.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Russh: Unchecked CryptoVec allocation and growth handling is reachable
CVE-2026-46673 / GHSA-g9f8-wqj9-fjw5
More information
Details
Title
Unchecked
CryptoVecallocation and growth handling was reachable from local agent inputs in currentrusshreleases and from remote SSH traffic in historical pre-0.58.0releasesSummary
CryptoVecused unchecked capacity growth, unchecked length arithmetic, and unsafe allocation/locking paths. In currentrusshreleases, local SSH agent peers could still feed attacker-controlled frame lengths into buffer growth before validation. In olderrusshreleases before0.58.0, remote SSH traffic also reachedCryptoVecthrough transport and compression buffers.Details
The underlying unsafe paths were in
CryptoVec:cryptovec/src/cryptovec.rscryptovec/src/platform/unix.rsmlock/munlockpreviously accepted zero-length calls and performed null-pointer validation inside theunsafeOS-call pathThere are two relevant reachability stories:
russhrussh/src/keys/agent/client.rsAgentClient::read_response()read a peer-suppliedu32length and then resizedself.bufto that value before reading the payloadrussh/src/keys/agent/server.rsConnection::run()read a peer-suppliedu32length and then resizedself.bufto that value before reading the payloadThis is the path that still existed in current
0.60.xreleases before the fix, although by then those buffers were no longerCryptoVec.russh712e32b(first released inv0.58.0), non-secret transport and compression buffers inrusshstill usedCryptoVec712e32bworktree by adding and running:cipher::tests::remote_packet_length_grows_transport_cryptovec_buffercompression::tests::remote_compressed_payload_expands_cryptovec_outputCryptoVecthrough:Also added a constrained-memory reproduction in that historical worktree:
compression::tests::remote_compressed_payload_can_crash_under_memory_limitThat test re-execs the test binary under
prlimit --as=134217728, decompresses a highly compressible payload that expands to96 MiB, and reliably aborts in the old UnixCryptoVecpath whenNonNull::new_unchecked()receives a null pointer after allocation failure.The prepared patch does two things:
hardens
CryptoVecitselfmlock/munlockno-opsunsafelocking callshardens the real untrusted-input path
256 * 1024on both client and server before resizing buffersThis cap matches OpenSSH’s agent framing guardrail.
PoC
The following end-to-end tests demonstrate the real untrusted-input path by feeding oversized peer-controlled agent frame lengths into the public client and server flows and asserting that they are rejected before buffer growth.
Client-side agent reply path:
Server-side agent request path:
These tests pass on the fixed branch and fail on unfixed
v0.60.2, where oversized agent frame lengths are not rejected at the framing boundary.For historical
russh < 0.58.0, I also verified remote reachability intoCryptoVecin a detached pre-712e32bworktree (91d431d, package version0.57.1).Transport packet read path:
Compression growth path:
Constrained-memory crash reproduction for the historical remote compression path:
On that historical worktree, the constrained-memory child aborts in the old Unix
CryptoVecpath with:To run the reproduced checks:
Historical pre-
0.58.0checks were run from the detached91d431dworktree with:Impact
This is a memory-safety hardening issue with demonstrated untrusted-input reachability.
What is demonstrated:
CryptoVecthrough transport and compression buffers inrussh < 0.58.0CryptoVeccodeWhat is not demonstrated:
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
russh: Post-decompression SSH packet size was not bounded, allowing remote oversized compressed packets
CVE-2026-46702 / GHSA-wwx6-x28x-8259
More information
Details
Summary
When SSH compression is enabled,
russhaccepted 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.In current releases, this is a remote denial-of-service / resource-exhaustion issue in the post-decompression receive path.
In older releases before
0.58.0, the same remote decompression path usedCryptoVec, which appears to make the historical impact worse.Details
The normal SSH transport read path enforces a packet-length limit before the packet body is read:
russh/src/cipher/mod.rsHowever, RFC 4253 compression is applied to the SSH
payloadfield only. Thepacket_lengthfield 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.In
russh, compressed packet bodies are later decompressed in:russh/src/compression.rsrussh/src/client/mod.rsrussh/src/server/session.rsBefore 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.It was verified that an attacker-crafted compressed payload can stay below the normal
256 KiBimplementation 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.Version detail:
russhas far back as0.34.0.>= 0.34.0, < 0.58.0, the remote decompression path still usedCryptoVec. Remote compressed SSH traffic could drive that path, and under constrained memory that historical code path could abort the process.>= 0.58.0, non-secret packet/decompression buffers were moved offCryptoVecand ontoVec<u8>, 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 olderCryptoVec-based abort story.0.60.2.Compression is not selected in a default-vs-default
russhsession because the default preference order putsnonefirst. However, the default server configuration still advertiseszlibandzlib@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 defaultrusshserver.OpenSSH portable was checked at
/home/mjc/projects/openssh-portablecommit45b30e0a5. OpenSSH enforces a256 KiBtransport packet cap before decompression, but it does not reuse that cap after decompression. Instead, decompression writes to ansshbuf, which is indirectly bounded by OpenSSH'sSSHBUF_SIZE_MAXhard maximum of0x8000000bytes (128 MiB).The 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.Relevant OpenSSH reference points:
/home/mjc/projects/openssh-portable/packet.c:PACKET_MAX_SIZE (256 * 1024)/home/mjc/projects/openssh-portable/packet.c:uncompress_buffer()inflates intocompression_buffer/home/mjc/projects/openssh-portable/sshbuf.h:SSHBUF_SIZE_MAX 0x8000000RFC / OpenSSH Comparison
RFC 4253 section 6 defines the binary packet format:
packet_lengthpadding_lengthpayloadRFC 4253 section 6.2 says that, when compression is negotiated, the
payloadfield is compressed, and thatpacket_lengthand 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.That 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.
OpenSSH provides such a bound indirectly through
sshbuf's hard maximum. Therusshfix should make the corresponding post-decompression bound explicit.PoC
There were two kinds of proof:
DEBUGpayload can stay below the normal SSH transport packet cap while still inflating beyond the intended post-decompression boundThe current in-tree regression tests are:
tests::compress::oversized_debug_payload_can_stay_below_wire_capcompression::tests::oversized_decompressed_packet_is_rejectedclient::tests::compressed_debug_is_ignored_after_client_parses_itclient::tests::oversized_compressed_debug_is_rejected_before_client_ignores_itserver::session::tests::compressed_debug_is_ignored_after_server_parses_itserver::session::tests::oversized_compressed_debug_is_rejected_before_server_ignores_itThe important behavior is:
DEBUGpayload can stay below the normal256 KiBtransport packet cap while still inflating beyond128 MiB.DEBUGpackets are still ignored normally after parsing.DEBUGpackets are rejected before the implementation reaches the normal "ignore DEBUG" behavior.The strongest PoC for severity is the unauthenticated server-side case. A malicious client can choose
zlibin the initial key exchange, because the default server advertises it and server-side negotiation follows the client's preference order for common algorithms. AfterNEWKEYS, but before authentication, the client can send a transport-layerSSH_MSG_DEBUGpacket whose compressed body is below the transport packet cap but whose decompressed body exceeds the post-decompression cap.That demonstrates the
AV:N/AC:L/PR:N/UI:Ncase directly: the attacker is a remote SSH client and does not need a successfully authenticated session.The equivalent wire-level attack shape is:
The 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 normalDEBUGignore handling. The server-side pre-auth variant above is the one that justifies the highest CVSS framing for this bug.The most important targeted checks are:
Before 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.
Impact
Suggested CVSS v3.1 for current maintained releases:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H7.5Reasoning:
AV:N: reachable by a remote SSH peerAC:L: straightforward once compression is enabledPR:N,UI:N: no prior auth or user interaction requiredC:N,I:N: confidentiality or integrity impact was not demonstratedA:H: remote peer can cause oversized post-decompression packet processing and disconnect / denial of serviceAffected versions:
russh >= 0.34.0, < 0.58.0russh >= 0.58.0, including0.60.3Fix / Patch Direction
Add an explicit maximum decompressed SSH packet size and enforce it inside
Decompress::decompress()before returning decompressed bytes to the client or server packet parser.The intended ceiling is
128 MiB, matching OpenSSH portable's effectivesshbufhard 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.The fix should preserve normal compressed packet behavior below the cap, including
DEBUGpackets that are decompressed and then ignored through the existing normal path.Patch branch:
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
russh server userauth state is not reset when authentication principal changes
CVE-2026-46705 / GHSA-hpv4-5h6f-wqr3
More information
Details
Summary
The
russhserver authentication path keeps internal userauth state acrossSSH_MSG_USERAUTH_REQUESTmessages without separating that state when the request principal changes.RFC 4252 allows the
user nameandservice namefields to change between authentication requests. The issue is not that such changes are invalid. The issue is that russh-owned authentication state, such as remaining methods, partial-success state, and in-progress method state, can remain associated with the connection and then influence a later request for a different(user, service).This is an internal library state mismatch. Applications are responsible for any authentication state they keep in their own handlers, but russh must reset or separate state that russh itself owns.
Details
The relevant server-side auth logic is in:
russh/src/server/encrypted.rsrussh/src/auth.rsRFC 4252 section 5 says the
user nameandservice namefields are repeated in everySSH_MSG_USERAUTH_REQUESTand may change. It also says the server implementation must check those fields in every message and flush accumulated authentication state if they change; if it cannot flush that state, it must disconnect.In vulnerable
russhcode, the username and service are decoded from eachSSH_MSG_USERAUTH_REQUEST, while theAuthRequeststate remains connection-scoped. That state includes:methods, which is later encoded as theSSH_MSG_USERAUTH_FAILUREremaining-methods list.partial_success, which is later encoded inSSH_MSG_USERAUTH_FAILURE.current, which tracks in-progress method state such as public-key offer or keyboard-interactive challenge state.rejection_count.If one request narrows russh's internal
methodsset, a later request for a different user can observe that narrowed set unless the internal state is reset at the principal boundary.PoC
The PoC demonstrates only russh-owned state. The handler does not store any cross-request state. Alice's request narrows russh's remaining methods to
password; Bob's later plain reject should not reuse that internal state.On
upstream/main, this fails with:That failure is produced by russh's retained
AuthRequest.methods; it does not depend on handler-owned MFA/session state.Impact
Suggested provisional CVSS v3.1:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N5.3Reasoning:
AV:N: reachable by a remote SSH client during authentication.AC:L: the attack is a normal sequence of SSH user-auth packets.PR:N: the attacker does not need an already-authenticated SSH session.UI:N: no user interaction is required on the server side.S:U: the impact is within the vulnerable SSH server implementation.C:N: the narrow PoC does not disclose confidential data.I:L: russh-owned authentication state for one principal can affect the authentication flow for a different principal.A:N: the narrow PoC does not demonstrate an availability impact.This report does not claim that username changes are inherently invalid, nor does it rely on application-owned authentication state being mishandled by the embedding server.
Fix / Patch Direction
The fix should update russh's internal userauth state handling so that accumulated russh-owned state is flushed or separated when
(user, service)changes betweenSSH_MSG_USERAUTH_REQUESTmessages.The fix stores the last seen
(user, service)onAuthRequest. When a new auth request arrives for a different principal, russh resets its internal auth state before dispatching the new request. This keeps username changes protocol-valid while preventing prior russh-owned auth state from carrying into the new principal.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Russh: Unchecked keyboard-interactive prompt count in client auth path
CVE-2026-48107 / GHSA-g9g7-5cgw-6v28
More information
Details
Summary
In the
russhclient keyboard-interactive authentication path, a malicious SSH server could send aUSERAUTH_INFO_REQUESTwith an attacker-controlled prompt count, and the client would use that raw count directly inVec::with_capacity(...)before validating that enough prompt data was actually present in the packet.This is a client-side denial-of-service / resource-exhaustion issue on the keyboard-interactive auth path.
Details
The vulnerable code path is in:
russh/src/client/encrypted.rsWhen the client is in
CurrentRequest::KeyboardInteractivestate and receivesSSH_MSG_USERAUTH_INFO_REQUEST, it parses:nameinstructionslanguage tagn_promptsBefore the fix, the code then did:
That means a malicious server could advertise an enormous
n_promptsvalue even if the packet contained no prompt bodies at all.The fix rejects inconsistent prompt counts before allocating:
Each prompt needs at least 4 bytes of string length plus 1 byte of echo flag, so
remaining_len() / 5is a safe upper bound. If the declared count exceeds what the packet can actually contain, the packet is malformed and is now rejected instead of being silently truncated.The tester did not find a same-class server-side bug in the reciprocal
USERAUTH_INFO_RESPONSEpath. The server already bounds the response count by remaining packet length before allocating.Affected package and versions:
russh0.37.00.60.2The tester does not believe this issue affects the other crates in this workspace (
russh-config,russh-cryptovec,pageant, orrussh-util).PoC
An in-tree regression test was added:
client::tests::oversized_keyboard_interactive_prompt_count_is_rejectedThe test builds a client session in
WaitingAuthRequest(KeyboardInteractive)state, feeds it a syntheticUSERAUTH_INFO_REQUESTpacket with:nameinstructionsn_prompts = u32::MAXOn the fixed code, the client rejects the packet with
Error::Inconsistentand does not emit a reply to the caller.For old-code impact verification, the pre-fix path was also checked separately with a constrained-memory repro. On unfixed
upstream/main, the same malformed packet attempted a very large allocation and failed with:Relevant verification commands:
Impact
Suggested CVSS v3.1:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H6.5Reasoning:
AV:N: reached by a malicious SSH server over the networkAC:L: the packet format is straightforwardPR:N: no prior authentication requiredUI:R: the victim must initiate a connection and proceed into keyboard-interactive authC:N,I:N: Confidentiality or integrity impact were not demonstratedA:H: the server can drive a very large allocation attempt in the client auth path, which can abort or exhaust client-side resources depending on allocator and platform behaviorSeverity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Russh: SSH identification parsing accepted non-canonical client banners and did not bound pre-banner input
CVE-2026-48108 / GHSA-76r6-x97p-67vr
More information
Details
Summary
russhdid not enforce the SSH identification-string rules as deliberately as OpenSSH. In particular, the server-side identification reader used the same permissive path as the client, allowing pre-banner lines from clients, and the reader did not enforce a bounded number of pre-banner lines.For a library server built on
russh, this could allow a remote peer to hold connection setup resources in the cleartext pre-authentication phase with malformed identification input that should have been rejected early.Details
RFC 4253 section 4.2 defines the SSH protocol version exchange. The identification string is a single line terminated by CR LF, must fit within 255 characters including CR LF, and clients should not send pre-banner lines before their SSH identification string.
Before the fix,
russh's identification reader lived in:russh/src/ssh_read.rsrussh/src/server/mod.rsThe same
read_ssh_id()behavior was used for both client and server contexts. That allowed server-side parsing to accept preliminary banner lines from clients, even though RFC 4253 only describes server-side pre-identification text. The reader also discarded preliminary lines without a line-count cap, so a peer could repeatedly send short non-SSH lines and keep the connection in identification parsing until an application-level timeout or external resource limit intervened.This also creates a remotely observable parser-state oracle inside a single connection. A client can send candidate identification lines one after another: lines not recognized as SSH identification are discarded as pre-banner text, while an accepted identification string terminates banner parsing and advances the connection into key exchange. A strict server would reject the first invalid client pre-banner line and force a reconnect for each probe. This can disclose server-side parser acceptance behavior and make fingerprinting cheaper, though it does not disclose application secrets, credentials, keys, or authenticated user data.
The patch splits the behavior between generic/server-banner-tolerant reading and stricter client-identification reading. It also adds explicit limits for line length and pre-banner line count.
Relevant branch commit:
3de4a68 Harden SSH identification parsingRFC / OpenSSH Comparison
RFC 4253 section 4.2 says each side sends an identification string of the form
SSH-protoversion-softwareversion SP comments CR LF. It allows a server to send other lines before its identification string, but says a client must be able to process such lines. It does not grant the same pre-banner allowance to clients.OpenSSH portable enforces explicit identification limits:
/home/mjc/projects/openssh-portable/ssh.h:SSH_MAX_BANNER_LEN/home/mjc/projects/openssh-portable/ssh.h:SSH_MAX_PRE_BANNER_LINES/home/mjc/projects/openssh-portable/kex.c: client-side handling of server pre-banner lines/home/mjc/projects/openssh-portable/ssh_api.c: rejects pre-banner lines when acting as a serverI checked
/home/mjc/projects/openssh-portableat45b30e0a5. OpenSSH uses an implementation banner line limit of8192bytes and a pre-banner line cap of1024, which is more permissive than RFC 4253's 255-character identification-string limit. The relevant alignment is the parser shape: OpenSSH permits bounded pre-banner lines when reading a server banner as a client, but rejects pre-banner lines when acting as a server and reading a client identification string.The
russhfix follows that shape: accept bounded pre-banner lines only where the protocol allows them, and reject malformed or excessive identification input early.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.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.
Impact
Suggested CVSS v3.1:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L5.3Reasoning:
AV:N: reachable over the SSH transport connectionAC:L: no special race or unusual setup is requiredPR:N,UI:N: occurs before authentication and needs no user interactionC:N,I:N: no confidentiality or integrity impact demonstratedA:L: malformed identification input can consume connection setup resources until rejected by timeout or external limitsAdditional impact investigation did not identify a stronger confidentiality, integrity, downgrade, or code-execution primitive. The accepted client pre-banner line is discarded before key exchange and does not become part of
remote_sshid; the final client identification string is what feeds the key-exchange transcript.remote_sshidis otherwise exposed to library handlers and debug formatting, but the discarded pre-banner text does not influence authentication state, strict-kex negotiation, KEX algorithm selection, or later packet framing.One parser-boundary nuance on vulnerable code is that behavior can depend on read chunking: if a client pre-banner line and the real SSH identification line are delivered in the same read, the old parser can discard the buffered identification line and then wait or disconnect; if delivered separately, the old server path can accept the pre-banner and continue. This supports malformed pre-authentication availability impact, but not a demonstrated confidentiality or integrity impact.
Fix / Patch Direction
Use a stricter server-side client-identification reader, enforce the RFC identification-line length, and cap preliminary banner lines. The server path should reject client pre-banner lines instead of treating them like allowed server pre-identification text.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Russh SSH message fields were decoded through allocation-first parsers before field-specific bounds
CVE-2026-48110 / GHSA-4r3c-5hpg-58qr
More information
Details
SSH message fields were decoded through allocation-first parsers before field-specific bounds
Summary
Several
russhclient and server message handlers decoded attacker-controlled SSH strings, name-lists, and byte fields into owned allocations before applying field-specific bounds. A remote SSH peer could send oversized, high-fanout, or malformed length-prefixed fields and make the library allocate, attempt to allocate, or split data before rejecting input that should have been rejected earlier.Affected Versions
Oldest verified exploitable stable release:
russh 0.34.0.russh >= 0.34.0, < 0.58.0. These releases have the allocation-first KEXINIT field parsing issue and still useCryptoVecfor inbound packet/decompression buffers. A peer can combine negotiated RFCzlib, rekey, compressed KEXINIT expansion, historicalCryptoVecdecompression growth, and KEXINIT name-list fanout.russh >= 0.58.0, including0.60.2. These releases moved non-secret packet/decompression buffers offCryptoVec, but the allocation-first SSH field parser issue remains reachable as aVec/String/name-list resource exhaustion issue.Prerelease coverage was not claimed for the zlib/
CryptoVec/KEXINIT combo because the combined historical exploit shape was verified against stablev0.34.0-era code and reproduced the stress behavior onv0.57.1.Details
The affected parser pattern appeared across the SSH transport and encrypted-message parser code:
Examples of allocation-first field parsing covered by the fix include:
USERAUTH_FAILUREmethod listsUSERAUTH_BANNERtext fieldsUSERAUTH_PK_OKfieldsEXT_INFOextension fieldsSERVICE_REQUESTnamesUSERAUTH_REQUESTheader fieldsBefore the fix, these handlers generally used
ssh_encoding::DecodeintoString,Bytes,Vec, orNameListfirst, then validated semantics later. For length-prefixed SSH fields, that means the owned decoder can accept an attacker-controlled length prefix and allocate or attempt allocation before discovering that the packet is truncated or above a local field bound. The fix introduces borrowed bounded parsing helpers such astake_str,take_bytes, andtake_name_list.RFC / OpenSSH Comparison
RFC 4251 section 5 defines SSH
stringandname-listencodings. RFC 4253 and RFC 4254 then use those encodings throughout KEX, auth, channel, and forwarding messages. The RFC encoding permits large length prefixes, so implementations need local bounds appropriate to their packet and parser model.RFC 4251 also says each name inside a
name-listis non-empty, cannot contain a comma, and is made of US-ASCII names. RFC 4253 section 7.1 requires the algorithm name-lists inSSH_MSG_KEXINITto contain at least one algorithm name, while language name-lists may be empty.OpenSSH portable commonly parses SSH fields with packet-buffer helpers and then immediately checks message completion:
openssh-portable:kex.c:kex_input_kexinit()/kex_buf2prop()openssh-portable:auth2.c:USERAUTH_REQUESTheader parsingopenssh-portable:sshconnect2.c: client auth reply parsingopenssh-portable:serverloop.c: global and channel-open parsingopenssh-portable:session.c: channel request parsingopenssh-portable:packet.c:sshpkt_get_cstring(),sshpkt_get_string(),sshpkt_get_end()openssh-portablewas checked at45b30e0a5. OpenSSH generally gets its size safety from the already-bounded packet buffer andsshbufhelpers; it does not always avoid allocating a copied field. Therusshpatch is stricter in Rust-specific shape by using borrowed bounded helpers where practical, but the protocol alignment is the same: reject oversized or malformed name-lists/strings within a bounded packet parser.PoC
Inline availability stress PoC: an unauthenticated client sends concurrent
SSH_MSG_KEXINITpayloads with a large but packet-sized first name-list containing many small algorithm names. This reaches the server-side initial key-exchange parser before user authentication and drives allocation-heavy owned decoding and name-list splitting. In a local direct-parser stress harness, 512 concurrent connection-equivalent parser workers parsing this payload eight times each raised process memory from about 4 MiB RSS to about 4.45 GiB RSS:That concurrency level is material: the multi-GiB result required 512 simultaneous connection-equivalent parser contexts and about 1.02 GiB of total input across the run. The harness exercises the vulnerable pre-auth KEXINIT parser directly rather than opening real sockets, but the parsed bytes are ordinary SSH KEXINIT payload bytes reachable from a remote unauthenticated SSH peer.
Historical pre-
0.58.0amplification note: before0.58.0, inbound packet and decompression buffers still usedCryptoVec. To get the stronger historical growth, the peer must negotiate RFCzlibcompression, complete the first key exchange, and then send a compressed rekeySSH_MSG_KEXINITcarrying the same high-fanout name-list shape. In av0.57.1harness, a 652-byte compressed rekey KEXINIT inflated to a 600,103-byte KEXINIT payload, grew the historicalCryptoVecdecompression output, and then entered the same allocation-heavy KEXINIT name-list parser:The constrained-memory result is useful because it shows where this becomes a service-killing failure rather than only elevated RSS. With the same historical code path, a roughly 1 KiB compressed rekey KEXINIT can force
CryptoVecdecompression growth into the parser fanout. Under an address-space limit, the process aborted on allocator failure while trying to satisfy one of the intermediate growth allocations:That historical result combines the field-parser issue in this report with the pre-
0.58.0CryptoVecallocation/growth behavior. The important maintainer takeaway is the amplification shape: very small compressed rekey packets can create much larger historicalCryptoVecbuffers and then immediately feed the unbounded KEXINIT name-list parser. It is included here to explain historical severity and exploit shape; the separate CryptoVec advisory covers the underlyingCryptoVecallocation/growth bug itself.