CVE-2026-102823: russh Client-side Channel-scoped Handler Callbacks Fire Vulnerability

HIGH
Advisory Identifier: GHSA-47hw-gvq5-r2gm
CVSS 7.5
CWE-20
Published
Advisory Summary

CVE-2026-102823 is a HIGH (CVSS 7.5) client-side channel-scoped handler callbacks fire vulnerability (CWE-20: Improper Input Validation) affecting russh libraries in the crates.io ecosystem. The issue involves client-side channel-scoped handler callbacks fire for channel ids the client never opened. Remediation is available by upgrading affected packages to version 0.63.1.

CVE-2026-102823 at a Glance

CVE Identifier CVE-2026-102823
GHSA Identifier GHSA-47hw-gvq5-r2gm
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-20: Improper Input Validation
Affected Product russh
Package Ecosystem crates.io
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity High (I:H)
Availability None (A:N)

How to Fix CVE-2026-102823

To remediate this vulnerability, upgrade all affected russh dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

russh fixed: 0.63.1
Upgrade russh to 0.63.1
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for russh

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-102823, inspect your project dependencies for russh:

inspect dependencies for russh

Analyze the terminal output to locate any version of russh in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.63.1), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-20: Improper Input Validation, where the product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly. In russh, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary CVE-2026-68930 was fixed by adding `Session::is_established_channel()` in `russh/src/server/encrypted.rs`, which gates every channel-scoped SERVER-side message (CHANNEL_REQUEST, CHANNEL_DATA, CHANNEL_EOF, CHANNEL_CLOSE, CHANNEL_WINDOW_ADJUST, CHANNEL_EXTENDED_DATA) on `enc.channels.get(&channel).is_some_and(|c| c.confirmed)` before invoking any `Handler` callback. The identical validation was never added to the CLIENT side (`russh/src/client/encrypted.rs`), which processes channel-scoped messages sent by the SSH SERVER once the client has authenticated. ### Details In `client_read_authenticated` (`russh/src/client/encrypted.rs`, ~lines 431-757), for CHANNEL_DATA, CHANNEL_EXTENDED_DATA, CHANNEL_EOF, CHANNEL_CLOSE, CHANNEL_OPEN_FAILURE, CHANNEL_SUCCESS, CHANNEL_FAILURE, and the CHANNEL_REQUEST sub-types exit-status/exit-signal/xon-xoff, the code only optionally forwards the event to the internal per-channel mpsc sender via `if let Some(chan) = self.channels.get(&channel_num) { ... }` (a no-op if the channel is unknown), but then **unconditionally** calls the corresponding public `Handler` trait method (`client.data(...)`, `client.exit_status(...)`, `client.channel_close(...)`, `client.channel_success(...)`, etc.) regardless of whether `channel_num` corresponds to any channel the client ever opened or that was ever confirmed. Only CHANNEL_OPEN_CONFIRMATION (closes the connection with `Error::Inconsistent` if unknown) and CHANNEL_WINDOW_ADJUST (returns early with `Ok(())` if unknown) correctly validate channel existence before acting. Corroborating evidence this check was intended but never wired up: `crate::Error` defines a dedicated `WrongChannel` variant documented as "Message received/sent on unopened channel" (`russh/src/lib_inner.rs`, ~line 144-146), yet a repo-wide search shows this variant is never constructed or returned anywhere in the codebase — dead code left over from (or intended for) exactly this validation. Because `Session::new_channel_id()` (`russh/src/session.rs`, ~line 708) allocates channel IDs sequentially starting at 1, a malicious or compromised SSH server can trivially predict the ID of the client's next channel and inject spoofed lifecycle events for it before or interleaved with the real channel-open exchange, or replay events for already-closed channel IDs. ### PoC Many real-world consumers of russh-as-a-client (deployment/orchestration tools, CI runners connecting to build/bastion hosts, git-over-ssh style tooling, database/tunnel clients) implement the `client::Handler` trait directly and key their own state (e.g. `HashMap<ChannelId, CommandState>`, exit-code trackers, per-channel byte counters, completion futures) off the channel IDs the library hands them, trusting the documented contract that events like "The remote process has exited" (`exit_status`) or "Called when the server closes a channel" (`channel_close`) only fire for a channel the application itself opened. A malicious, MITM'd (via a compromised/rogue jump host the client is configured to trust), or simply hostile SSH server can send `SSH_MSG_CHANNEL_REQUEST` (exit-status/exit-signal), `SSH_MSG_CHANNEL_DATA`, `SSH_MSG_CHANNEL_CLOSE`, `SSH_MSG_CHANNEL_SUCCESS`/`FAILURE`, or `SSH_MSG_CHANNEL_OPEN_FAILURE` for an arbitrary/predicted/never-opened channel ID at any point after authentication completes. Because the library invokes the `Handler` callback unconditionally, this reaches application code with an ID it never registered. ### Impact (1) A reliable, purely protocol-level trigger for an application panic/DoS in any client that indexes per-channel state by `ChannelId` without itself re-checking channel validity — the exact class of bug CVE-2026-68930 fixed server-side; and (2) lets the server spoof exit-status/exit-signal/close/success/failure notifications for a channel the client has not yet opened or has already released, desynchronizing the client's command-completion bookkeeping (e.g. reporting a forged exit code 0 for a not-yet-run remote command, or a premature `channel_close` before real output/exit-status has arrived) — a business-logic-level integrity violation of the SSH channel lifecycle that automation built on russh implicitly relies on. Suggested fix: add the same `is_established_channel()`-style gate already used in `server/encrypted.rs` to `client/encrypted.rs`'s `client_read_authenticated`, checking `self.channels.get(&channel_num)` before invoking any `Handler` callback (not just the mpsc forward), for every channel-scoped message type. For credit/changelog purposes, please use: Yazan Balawneh, Cystack.ps

CVE-2026-102823 Affected russh Versions

1 component(s)
Fixed Versions:
0.63.1
Product / Package Ecosystem Affected Versions Fixed Version
russh
crates.io
< 0.63.1
0.63.1

References

Frequently Asked Questions

What is CVE-2026-102823?
CVE-2026-102823 is a security vulnerability in russh involving Client-side channel-scoped Handler callbacks fire for channel IDs the client never opened under CWE-20: Improper Input Validation.
Which russh versions are affected?
Affected versions include releases prior to 0.63.1 across affected packages: russh.
What version fixes CVE-2026-102823?
Upgrading to 0.63.1 (or the latest patched release for your respective branch) resolves the vulnerability.
Is russh affected?
Yes, russh is affected in releases prior to 0.63.1. Upgrade your dependency to 0.63.1 or later.
What is the CVSS score of CVE-2026-102823?
CVE-2026-102823 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N.
What CWE is associated with CVE-2026-102823?
CVE-2026-102823 is classified under CWE-20: Improper Input Validation (The product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly.).
How can I check crates.io dependencies for CVE-2026-102823?
Run the dependency tree command: 'inspect dependencies for russh'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-47hw-gvq5-r2gm",
  "modified" : "2026-09-30T23:27:17Z",
  "published" : "2026-09-30T23:27:17Z",
  "aliases" : [ "CVE-2026-102823" ],
  "summary" : "russh: Client-side channel-scoped Handler callbacks fire for channel IDs the client never opened",
  "details" : "### Summary\nCVE-2026-68930 was fixed by adding `Session::is_established_channel()` in `russh/src/server/encrypted.rs`, which gates every channel-scoped SERVER-side message (CHANNEL_REQUEST, CHANNEL_DATA, CHANNEL_EOF, CHANNEL_CLOSE, CHANNEL_WINDOW_ADJUST, CHANNEL_EXTENDED_DATA) on `enc.channels.get(&channel).is_some_and(|c| c.confirmed)` before invoking any `Handler` callback. The identical validation was never added to the CLIENT side (`russh/src/client/encrypted.rs`), which processes channel-scoped messages sent by the SSH SERVER once the client has authenticated.\n\n### Details\nIn `client_read_authenticated` (`russh/src/client/encrypted.rs`, ~lines 431-757), for CHANNEL_DATA, CHANNEL_EXTENDED_DATA, CHANNEL_EOF, CHANNEL_CLOSE, CHANNEL_OPEN_FAILURE, CHANNEL_SUCCESS, CHANNEL_FAILURE, and the CHANNEL_REQUEST sub-types exit-status/exit-signal/xon-xoff, the code only optionally forwards the event to the internal per-channel mpsc sender via `if let Some(chan) = self.channels.get(&channel_num) { ... }` (a no-op if the channel is unknown), but then **unconditionally** calls the corresponding public `Handler` trait method (`client.data(...)`, `client.exit_status(...)`, `client.channel_close(...)`, `client.channel_success(...)`, etc.) regardless of whether `channel_num` corresponds to any channel the client ever opened or that was ever confirmed. Only CHANNEL_OPEN_CONFIRMATION (closes the connection with `Error::Inconsistent` if unknown) and CHANNEL_WINDOW_ADJUST (returns early with `Ok(())` if unknown) correctly validate channel existence before acting.\n\nCorroborating evidence this check was intended but never wired up: `crate::Error` defines a dedicated `WrongChannel` variant documented as \"Message received/sent on unopened channel\" (`russh/src/lib_inner.rs`, ~line 144-146), yet a repo-wide search shows this variant is never constructed or returned anywhere in the codebase — dead code left over from (or intended for) exactly this validation.\n\nBecause `Session::new_channel_id()` (`russh/src/session.rs`, ~line 708) allocates channel IDs sequentially starting at 1, a malicious or compromised SSH server can trivially predict the ID of the client's next channel and inject spoofed lifecycle events for it before or interleaved with the real channel-open exchange, or replay events for already-closed channel IDs.\n\n### PoC\nMany real-world consumers of russh-as-a-client (deployment/orchestration tools, CI runners connecting to build/bastion hosts, git-over-ssh style tooling, database/tunnel clients) implement the `client::Handler` trait directly and key their own state (e.g. `HashMap<ChannelId, CommandState>`, exit-code trackers, per-channel byte counters, completion futures) off the channel IDs the library hands them, trusting the documented contract that events like \"The remote process has exited\" (`exit_status`) or \"Called when the server closes a channel\" (`channel_close`) only fire for a channel the application itself opened.\n\nA malicious, MITM'd (via a compromised/rogue jump host the client is configured to trust), or simply hostile SSH server can send `SSH_MSG_CHANNEL_REQUEST` (exit-status/exit-signal), `SSH_MSG_CHANNEL_DATA`, `SSH_MSG_CHANNEL_CLOSE`, `SSH_MSG_CHANNEL_SUCCESS`/`FAILURE`, or `SSH_MSG_CHANNEL_OPEN_FAILURE` for an arbitrary/predicted/never-opened channel ID at any point after authentication completes. Because the library invokes the `Handler` callback unconditionally, this reaches application code with an ID it never registered.\n\n### Impact\n(1) A reliable, purely protocol-level trigger for an application panic/DoS in any client that indexes per-channel state by `ChannelId` without itself re-checking channel validity — the exact class of bug CVE-2026-68930 fixed server-side; and (2) lets the server spoof exit-status/exit-signal/close/success/failure notifications for a channel the client has not yet opened or has already released, desynchronizing the client's command-completion bookkeeping (e.g. reporting a forged exit code 0 for a not-yet-run remote command, or a premature `channel_close` before real output/exit-status has arrived) — a business-logic-level integrity violation of the SSH channel lifecycle that automation built on russh implicitly relies on.\n\nSuggested fix: add the same `is_established_channel()`-style gate already used in `server/encrypted.rs` to `client/encrypted.rs`'s `client_read_authenticated`, checking `self.channels.get(&channel_num)` before invoking any `Handler` callback (not just the mpsc forward), for every channel-scoped message type.\n\nFor credit/changelog purposes, please use: Yazan Balawneh, Cystack.ps",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "crates.io",
      "name" : "russh"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.63.1"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 0.63.0"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/Eugeny/russh/security/advisories/GHSA-47hw-gvq5-r2gm"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-102823"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Eugeny/russh/commit/3430fd26ecafc0dc3705210f5f39a9119fa22774"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/Eugeny/russh"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Eugeny/russh/releases/tag/v0.63.1"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-20" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-30T23:27:17Z",
    "nvd_published_at" : "2026-09-29T19:17:24Z"
  }
}