# CVE-2026-102823: russh: Client-side channel-scoped Handler callbacks fire for channel IDs the client never opened

> CVE-2026-102823 (GitHub advisory GHSA-47hw-gvq5-r2gm) is a high-severity vulnerability with a CVSS 3.1 base score of 7.5, classed as Improper Input Validation (CWE-20), in russh (crates.io). Affected versions: <= 0.63.0. It is fixed in 0.63.1.

Canonical page: https://cveminer.com/vulnerabilities/CVE-2026-102823

| Field | Value |
| --- | --- |
| CVE | CVE-2026-102823 |
| GHSA | GHSA-47hw-gvq5-r2gm |
| Severity | High |
| CVSS 3.1 | 7.5 `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N` (GitHub Advisory Database) |
| CWE | CWE-20 Improper Input Validation |
| EPSS | 0.26% (higher than 16% of CVEs) |
| CISA KEV | Not listed |
| SSVC | exploitation poc, automatable yes, technical impact partial |
| Published | 29 Sept 2026 |
| Modified | 2 Oct 2026 |

## Description

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

## Affected packages

| Package | Ecosystem | PURL | Vulnerable versions | Fixed in |
| --- | --- | --- | --- | --- |
| russh | crates.io | `pkg:cargo/russh` | <= 0.63.0 | 0.63.1 |

## Fix commits

- Patch: https://github.com/Eugeny/russh/commit/3430fd26ecafc0dc3705210f5f39a9119fa22774
- Release: https://github.com/eugeny/russh/commit/d3ae702a43a163946f258297e398dc216339d5ce (0.63.1)

## Questions

**What is the GHSA ID for CVE-2026-102823?**

GitHub tracks CVE-2026-102823 as GHSA-47hw-gvq5-r2gm.

**What is the CVSS score of CVE-2026-102823?**

CVE-2026-102823 has a CVSS 3.1 base score of 7.5 (High), vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N.

**Which versions are affected by CVE-2026-102823?**

russh (crates.io): <= 0.63.0.

**How do I fix CVE-2026-102823?**

Upgrade to 0.63.1 or later. The fix is in commit 3430fd2.

**What is the Package URL (PURL) for CVE-2026-102823?**

The affected package's PURL is pkg:cargo/russh.

**Is CVE-2026-102823 exploited in the wild?**

CISA's Known Exploited Vulnerabilities catalog doesn't list CVE-2026-102823. That means no confirmed exploitation has been catalogued, not that it is safe to leave unpatched. CISA's SSVC assessment rates exploitation as "poc". EPSS estimates a 0.26% chance of exploitation in the next 30 days, higher than 16% of CVEs.

## References

- https://github.com/Eugeny/russh/commit/3430fd26ecafc0dc3705210f5f39a9119fa22774 (fix)
- https://github.com/Eugeny/russh/security/advisories/GHSA-47hw-gvq5-r2gm (advisory)
- https://nvd.nist.gov/vuln/detail/CVE-2026-102823 (advisory)
- https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/102xxx/CVE-2026-102823.json (advisory)
- https://github.com/advisories/GHSA-47hw-gvq5-r2gm (advisory)
- https://github.com/Eugeny/russh (package)
- https://github.com/Eugeny/russh/releases/tag/v0.63.1

---

Source: CVEminer (https://cveminer.com/vulnerabilities/CVE-2026-102823), compiled from OSV (GitHub advisory), GitHub Advisory Database, OSV (CVE record), CVE.org, CISA KEV catalog. OSV and GitHub Advisory Database content is licensed CC BY 4.0.
