{"id":"GHSA-47hw-gvq5-r2gm","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\u003cChannelId, CommandState\u003e`, 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","aliases":["CVE-2026-102823"],"modified":"2026-09-30T23:45:04.016651269Z","published":"2026-09-30T23:27:17Z","database_specific":{"nvd_published_at":"2026-09-29T19:17:24Z","cwe_ids":["CWE-20"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-09-30T23:27:17Z"},"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"}],"affected":[{"package":{"name":"russh","ecosystem":"crates.io","purl":"pkg:cargo/russh"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.63.1"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 0.63.0","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-47hw-gvq5-r2gm/GHSA-47hw-gvq5-r2gm.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N"}]}