Nov 14, 2026·10 min read·1 visit
In russh versions 0.55.0 through 0.65.0, the client::Handler::kex_done() callback is invoked with the raw SSH shared secret before check_server_key() verifies the remote server's identity. Applications using kex_done() to initialize secondary channels or process shared secrets can be forced to interact with an unauthenticated server. Fixed in version 0.65.1.
CVE-2026-128087 (GHSA-9264-jvgf-6w2f) is an incorrect behavior order (CWE-696) and improper authorization (CWE-863) vulnerability in the russh Rust SSH library, affecting versions 0.55.0 through 0.65.0. During the initial SSH key exchange (KEX) handshake, the client state machine invokes the client::Handler::kex_done() callback—providing the negotiated raw shared secret bytes—prior to validating the remote server's host key or certificate via check_server_key(). Consequently, an untrusted or rogue SSH server can complete cryptographic key agreement and force a connecting client to execute side-effecting application logic inside kex_done() before the server's identity is checked and rejected.
CVE-2026-128087 (tracked in the GitHub Advisory Database as GHSA-9264-jvgf-6w2f) is a state machine ordering vulnerability within russh, an asynchronous SSH client and server library implemented in Rust. The library provides core protocol handling for RFC 4253 (SSH Transport Layer Protocol), RFC 4252 (SSH Authentication Protocol), and RFC 4254 (SSH Connection Protocol). To support advanced protocol bridging and hybrid cryptographic architectures, russh exposes an event-driven trait interface, client::Handler, which allows consuming applications to hook into discrete phases of the SSH lifecycle.
In version 0.55.0 (introduced via commit 500bef1eccc1c48ac674e2a262aa7887777431eb), russh added the client::Handler::kex_done() method. This hook provides applications with direct access to the raw shared secret ($K$) negotiated during the Diffie-Hellman (DH) or Elliptic Curve Diffie-Hellman (ECDH) key exchange, alongside the negotiated algorithm identifiers. Developers utilize this callback to derive auxiliary cryptographic keys for secondary encrypted data planes, out-of-band UDP tunnels, or custom multiplexing protocols bound to the primary SSH session.
The vulnerability is classified under CWE-696 (Incorrect Behavior Order) and CWE-863 (Incorrect Authorization). During the initial SSH handshake in russh versions 0.55.0 through 0.65.0, the client packet processor dispatches kex_done() immediately upon computing the key exchange parameters, prior to invoking client::Handler::check_server_key(). Because check_server_key() is responsible for authenticating the server's public host key against trusted records (such as a known_hosts file or a certificate authority), the application receives and processes the shared secret before establishing trust in the remote peer.
As a result, an unauthenticated remote attacker operating a rogue SSH server—or intercepting traffic via a machine-in-the-middle (MitM) position—can complete the mathematical key exchange using an arbitrary, self-generated host key pair. The vulnerable russh client verifies the cryptographic signature over the exchange hash using the attacker's public key, triggers kex_done() with the resulting shared secret, and only afterward terminates the handshake when check_server_key() rejects the unrecognized key. Any side effects, network transmissions, or state changes executed within kex_done() occur against an untrusted endpoint.
To understand the root cause of CVE-2026-128087, it is necessary to distinguish between cryptographic proof-of-possession and peer identity authentication within the SSH Transport Layer Protocol (RFC 4253). During an SSH key exchange (such as curve25519-sha256), the client and server exchange ephemeral public values to compute a shared secret $K$ and an exchange hash $H$. The server transmits its public host key ($K_S$) and a signature $s$ over $H$ generated with its private host key. When the client verifies $s$ using $K_S$, it confirms only that the remote peer possesses the private key corresponding to $K_S$ and that the key exchange parameters were not modified in transit.
Cryptographic verification of the exchange hash signature does not establish whether $K_S$ belongs to the intended host. In russh, identity verification is delegated to the consuming application via the asynchronous trait method client::Handler::check_server_key(). The library expects this method to inspect the presented PublicKey and return Ok(true) if the host is trusted, or Ok(false) to abort the connection with Error::UnknownKey. Security invariants require that no sensitive session state or application-level callbacks depending on peer authenticity execute until check_server_key() returns Ok(true).
The flaw resides in the reply function within russh/src/client/mod.rs, which processes incoming SSH transport packets during key exchange completion (SSH_MSG_NEWKEYS). In vulnerable versions, russh handles both initial key exchanges and mid-session rekeying within the same code block. The diferenciator between an initial handshake and a rekey operation is whether session.common.encrypted is None (initial handshake) or Some(...) (active, already-authenticated session undergoing rekeying).
When kex_done() was implemented, the invocation was placed at the top of the SSH_MSG_NEWKEYS handling block, prior to the conditional branch checking session.common.encrypted.is_some(). For an initial connection, check_server_key() was located inside the else branch (session.common.encrypted.is_none()). This placement caused the state machine to execute handler.kex_done() unconditionally before reaching the host key verification logic.
The vulnerability was resolved in commit 43a46d67a80a7355fcd2fb25e7e00d79aec8184c (merged via Pull Request #808 and released in russh version 0.65.1). The patch modifies russh/src/client/mod.rs by relocating the handler.kex_done() call inside the respective branches of the if session.common.encrypted.is_some() check, ensuring that initial key exchange verification completes before the callback is triggered.
// File: russh/src/client/mod.rs (Vulnerable Code < 0.65.1)
session.common.strict_kex =
session.common.strict_kex || newkeys.names.strict_kex();
// VULNERABILITY: kex_done is invoked here before checking if this is an
// initial connection that still requires check_server_key() validation.
let shared_secret = newkeys.kex.shared_secret_bytes();
handler
.kex_done(shared_secret, &newkeys.names, session)
.await?;
if session.common.encrypted.is_some() {
// This is a rekey on an already verified session
let common = &mut session.common;
common.newkeys(newkeys);
// ...
} else {
// Initial handshake: check_server_key is called AFTER kex_done!
// ...
if !handler.check_server_key(server_key).await? {
return Err(Error::UnknownKey);
}
session
.common
.encrypted(initial_encrypted_state(session), newkeys);
}In the patched implementation, the premature invocation is removed from the top of the block and duplicated into the specific execution paths where trust has already been established:
File: russh/src/client/mod.rs
@@ -2033,14 +2033,11 @@ async fn reply<H: Handler>(
session.common.strict_kex =
session.common.strict_kex || newkeys.names.strict_kex();
- // Call the kex_done handler before consuming newkeys
- let shared_secret = newkeys.kex.shared_secret_bytes();
- handler
- .kex_done(shared_secret, &newkeys.names, session)
- .await?;
-
if session.common.encrypted.is_some() {
// This is a rekey
+ handler
+ .kex_done(newkeys.kex.shared_secret_bytes(), &newkeys.names, session)
+ .await?
{
let common = &mut session.common;
common.newkeys(newkeys);
@@ -2077,6 +2074,10 @@ async fn reply<H: Handler>(
}
}
+ handler
+ .kex_done(newkeys.kex.shared_secret_bytes(), &newkeys.names, session)
+ .await?;
+
session
.common
.encrypted(initial_encrypted_state(session), newkeys);> [!NOTE]
> Fix Completeness Assessment: The patch completely resolves the state ordering flaw. During a rekey operation (session.common.encrypted.is_some()), the remote peer's host key was already verified during the initial handshake, and all packets are traversing an authenticated encryption-with-associated-data (AEAD) or MAC-protected tunnel; thus, calling kex_done() immediately upon rekey completion is safe. During the initial handshake (session.common.encrypted.is_none()), kex_done() is now positioned strictly after the check_server_key() check (and its corresponding early return on error or false), ensuring that rejected servers never trigger the callback.
Exploiting CVE-2026-128087 requires specific architectural conditions on the target client and network positioning by the attacker. Because the default implementation of client::Handler::kex_done() in russh is a no-op that immediately returns Ok(()), standard SSH client applications that only override check_server_key() and standard channel methods are not exploitable. The target application must explicitly implement custom logic within kex_done() that performs external actions, allocates restricted resources, or transmits data over secondary channels using keys derived from shared_secret_bytes().
To execute the attack, an adversary must induce the vulnerable russh client to initiate an SSH connection to an attacker-controlled server endpoint. This can be achieved via network-level interception (such as ARP spoofing, BGP hijacking, or DNS poisoning) or by supplying a malicious host URI to an automated client service. No pre-existing credentials or knowledge of the legitimate server's private host key are required.
When the victim client connects, the rogue server negotiates a standard key exchange algorithm (e.g., curve25519-sha256) and generates an ephemeral key pair alongside a self-generated host key pair. The rogue server computes the shared secret $K$ and exchange hash $H$, signs $H$ with its untrusted host private key, and sends the KEX reply followed by SSH_MSG_NEWKEYS.
Upon receiving the server's response, the vulnerable russh client verifies the mathematical validity of the signature against the attacker's supplied public key and immediately invokes handler.kex_done(shared_secret, &newkeys.names, session). At this point, both the attacker and the victim client possess the identical shared secret $K$. If the client application's kex_done() implementation immediately initiates an out-of-band connection (for example, sending an encrypted registration payload or session token over a UDP data plane keyed with $K$), the attacker can decrypt and inspect this payload. Milliseconds later, russh invokes check_server_key(), detects the untrusted host key, and drops the TCP connection, leaving the attacker with whatever state or data was exposed during the premature callback window.
CVE-2026-128087 is assigned a CVSS v3.1 base score of 3.7 (Low) with the vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N. The Attack Vector (AV:N) is Network, as the SSH protocol operates remotely over TCP/IP. Privileges Required (PR:N) and User Interaction (UI:N) are both None, reflecting that automated client daemons or programmatic connections trigger the flaw during the pre-authentication handshake phase without human intervention.
The score is primarily constrained by Attack Complexity: High (AC:H) and limited impact metrics. High attack complexity reflects two necessary preconditions: the attacker must first intercept the network path or redirect the client to a rogue server, and the target application must explicitly override the optional kex_done() trait method with side-effecting logic. Applications using russh purely for standard SSH command execution, SFTP, or TCP port forwarding do not implement kex_done() and experience zero impact.
For applications that do utilize kex_done() to derive keys for auxiliary protocols, the impact is rated as Confidentiality: Low (C:L), with Integrity (I:N) and Availability (A:N) unaffected on the primary SSH session. While the primary SSH connection always terminates cleanly with Error::UnknownKey upon failing check_server_key(), any confidential metadata, handshake tokens, or early application payloads transmitted over secondary channels initialized inside kex_done() are compromised and readable by the rogue server.
From a threat intelligence perspective, the Exploit Prediction Scoring System (EPSS) assigns CVE-2026-128087 a score of 0.00239 (0.24%), placing it in the 15.05th percentile. There are no known public exploits, weaponized Proof-of-Concept (PoC) scripts, or active exploitation campaigns reported in the wild, and the vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog.
To remediate CVE-2026-128087, developers and system administrators maintaining Rust projects that depend on russh must upgrade the crate to version 0.65.1 or later. In version 0.65.1, the state machine guarantees that client::Handler::kex_done() is only invoked on an initial connection after client::Handler::check_server_key() has evaluated and accepted the server's public key.
Update the dependency specification in your project's Cargo.toml file and rebuild the application binary:
[dependencies]
russh = ">= 0.65.1"After updating Cargo.toml, run cargo update -p russh and verify that the resolved version in Cargo.lock is at least 0.65.1. You can audit your dependency tree for vulnerable versions using cargo audit (which cross-references the RustSec Advisory Database and GHSA-9264-jvgf-6w2f).
> [!NOTE]
> Workarounds for Unpatched Environments: If an immediate upgrade to russh >= 0.65.1 is blocked by dependency constraints, developers must refactor their client::Handler implementations to avoid executing side effects within kex_done(). Instead of initiating secondary connections or transmitting data directly inside kex_done(), store the derived key material in an internal struct field on your Handler implementation (marked as unverified), and defer any network transmission or resource allocation until after check_server_key() returns Ok(true) or until client authentication completes.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
russh Eugeny (warp-tech) | >= 0.55.0, < 0.65.1 | 0.65.1 |
| Attribute | Detail |
|---|---|
| CVE / Advisory ID | CVE-2026-128087 / GHSA-9264-jvgf-6w2f |
| Weakness Class | CWE-696 (Incorrect Behavior Order) / CWE-863 (Incorrect Authorization) |
| Affected Component | russh crate (russh::client::Handler::kex_done) |
| CVSS v3.1 Score | 3.7 Low (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N) |
| EPSS Score | 0.24% (15.05th Percentile) |
| Impact | Premature shared secret callback execution against unauthenticated SSH servers |
| Exploit & KEV Status | No known public exploits; Not listed in CISA KEV |
| Patched Version | russh >= 0.65.1 |
The software performs a sequence of behaviors in an order that violates security invariants. Specifically, the shared secret derivation callback is executed prior to authenticating the remote peer's identity via host key verification.
CVE-2026-128082 (GHSA-6vhm-j9h7-f9gh) is an uncontrolled recursion vulnerability (CWE-674) in AutoMapper, a convention-based object-object mapper for .NET. When mapping nested or self-referential types into destination models that rely on parameterized constructors, C# positional records, or recursive ConstructUsing expressions, AutoMapper evaluates child constructor arguments prior to incrementing the parent's recursion depth counter. This ordering flaw bypasses both explicit MaxDepth limits and the default depth limit of 64. An unauthenticated remote attacker can supply deeply nested input payloads to affected endpoints, exhausting the thread call stack and triggering an uncatchable System.StackOverflowException that terminates the hosting .NET process.
An information exposure vulnerability in the @opentelemetry/instrumentation-http package for OpenTelemetry JavaScript versions 0.204.0 prior to 0.222.0 causes incoming HTTP server spans to record raw, unredacted query string parameters under the url.query attribute. While outgoing client spans applied built-in and user-configured query parameter redaction, incoming server spans bypassed this sanitization completely, resulting in the transmission of sensitive credentials such as pre-signed cloud URLs and API tokens to external observability backends.
CVE-2026-128089 is an uncontrolled resource consumption vulnerability in the dnspython toolkit for Python, affecting all versions prior to 2.9.2. When parsing Well-Known Service (WKS) records from text via dns.rdtypes.IN.WKS.WKS.from_text(), the parser fails to validate that numeric service port identifiers fall within the RFC 1035 valid range of 0 through 65535. An attacker capable of supplying untrusted zone files or RDATA text strings containing an arbitrarily large service port integer can force the parser to allocate an unbounded bitmap array using an inefficient linear append loop, resulting in severe CPU exhaustion, application hangs, or Out-Of-Memory (OOM) process termination.
CVE-2026-126347 (GHSA-5cjw-f85j-cv6r) is an open redirect vulnerability (CWE-601) in Tornado, an asynchronous Python networking library and web framework. The flaw resides in tornado.web.RedirectHandler when dynamic URL redirection templates incorporate unvalidated user input. Due to a parser differential between RFC 3986 (used by Python's urllib.parse.urlsplit) and the WHATWG URL Standard (used by modern web browsers), inputs containing three or more leading slashes bypass Tornado 6.6.0's validation logic, allowing unauthenticated attackers to redirect users to arbitrary external domains.
A Denial of Service (DoS) vulnerability exists in @sveltejs/adapter-node versions 5.5.7 and 6.0.0-next.0 through 6.0.0. When a SvelteKit server route returns a direct response from a fetch() call with a Content-Type of text/event-stream, the Node.js adapter attempts to mutate the response headers to disable proxy buffering. Because headers returned directly by the WHATWG Fetch API carry an immutable guard, this mutation throws an unhandled TypeError, terminating the underlying Node.js process and denying service to all active and subsequent clients.
CVE-2026-128122 (GHSA-5wg4-q3ww-283h) is a build-time CRLF and wildcard injection vulnerability in `@sveltejs/adapter-cloudflare`. During the prerendering phase of a SvelteKit build, the adapter writes crawled redirect paths directly into the Cloudflare `_redirects` and `_routes.json` configuration files without output encoding or character validation. An attacker capable of controlling link paths on prerendered pages can inject newline characters, spaces, or wildcard patterns to forge arbitrary edge redirection rules, enabling asset hijacking and Stored Cross-Site Scripting (XSS).