{"schema":"threatline.record/2","record":{"id":"CVE-2026-102823","aliases":["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<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","categories":["Libraries & packages"],"product":"russh","vendor":"Eugeny","ecosystem":"rust","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-47hw-gvq5-r2gm","source_modified_at":"2026-09-30T23:27:18Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"rust","name":"russh"},"range":"<= 0.63.0"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-102823","source_modified_at":"2026-10-02T12:20:10.605Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"russh","vendor":"Eugeny","versions":[{"status":"affected","version":"< 0.63.1"}],"defaultStatus":null},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-47hw-gvq5-r2gm","source_modified_at":"2026-09-30T23:45:04.016651269Z","fetched_at":"2026-10-23T18:48:36.087640Z","package":{"name":"russh","ecosystem":"crates.io","purl":"pkg:cargo/russh"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.63.1"}]}],"versions":[]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-47hw-gvq5-r2gm","source_modified_at":"2026-09-30T23:27:18Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"rust","name":"russh"},"version":"0.63.1"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-47hw-gvq5-r2gm","source_modified_at":"2026-09-30T23:45:04.016651269Z","fetched_at":"2026-10-23T18:48:36.087640Z","package":{"name":"russh","ecosystem":"crates.io","purl":"pkg:cargo/russh"},"version":"0.63.1"}],"cvss":7.5,"severity":"High","kev":{"listed":false},"epss":{"probability":0.00261,"percentile":0.1695,"score_date":"2026-10-23"},"published_at":"2026-09-29T18:25:58.084Z","modified_at":"2026-10-02T12:20:10.605Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-47hw-gvq5-r2gm","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-09-30T23:27:18Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-102823","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-02T12:20:10.605Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-47hw-gvq5-r2gm","fetched_at":"2026-10-23T18:48:36.087640Z","source_modified_at":"2026-09-30T23:45:04.016651269Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-102823","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/Eugeny/russh/security/advisories/GHSA-47hw-gvq5-r2gm","tags":["exploit"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-102823","source":"GitHub Advisory Database"},{"source":"CVE List","url":"https://github.com/Eugeny/russh/commit/3430fd26ecafc0dc3705210f5f39a9119fa22774","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/Eugeny/russh/releases/tag/v0.63.1","tags":["x_refsource_MISC"]},{"url":"https://github.com/advisories/GHSA-47hw-gvq5-r2gm","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"poc","automatable":"yes","technical_impact":"partial","assessed_at":"2026-10-02T12:19:44.206468Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"ecosystem","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-02T12:20:10.605Z","content_hash":"f722c6200044e2e2c865b2426aa6035798fcec2c59bf037506e85ed62f197f9b"}}
