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.
Published 29 Sept 2026Updated 2 Oct 2026
- CVE ID
- CVE-2026-102823
- GHSA ID
- GHSA-47hw-gvq5-r2gm
- EPSS
- 0.26%higher than 16% of CVEs
- Weakness (CWE)
- CWE-20Improper Input Validation
- KEV status
- Not in KEV
- Published
- 29 Sept 2026
- Updated
- 2 Oct 2026
- Ecosystem
- crates.io (Rust)
- Assigned by (CNA)
- GitHub_M
Summary
Source: OSV (GitHub advisory)
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
Technical details
Source: GitHub Advisory Database
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:NHigh7.5Base score from GitHub Advisory Database
- Attack Vector AV
- Network
- Attack Complexity AC
- Low
- Privileges Required PR
- None
- User Interaction UI
- None
- Scope S
- Unchanged
- Confidentiality C
- None
- Integrity I
- High
- Availability A
- None
CISA SSVC decision points
- Exploitation
- poc
- Proof-of-concept code is public
- Automatable
- yes
- Attackers can automate this at scale
- Technical impact
- partial
- Limited control of the affected system
CISA Coordinator assessment (SSVC 2.0.3) via CVE.org.
Affected packages
Source: OSV (GitHub advisory)
- russhcrates.io (Rust)
pkg:cargo/russh- Affected versions
- <= 0.63.0
- Fixed in
- 0.63.1
| Package | Affected versions | Fixed in |
|---|---|---|
russh crates.io (Rust) pkg:cargo/russh |
|
|
Fix
Source: OSV (GitHub advisory)
Fix available
Upgrade to 0.63.1 or later.
Patch commits
correctly handle unexpected channel IDs
+53 −23 across 3 files
- russh/src/client/encrypted.rs+34 −2
- russh/src/server/encrypted.rs+5 −21
- russh/src/session.rs+14 −0
Release commits
The commits each fixed release tag points at. They usually bump the version rather than contain the fix itself.
Vulnerability intelligence
Root cause analysis
Vulnerable functions
References
Advisories 4
Package 1
Other links 1
Credits
- sonicnewreporter
Questions about CVE-2026-102823
- 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.
Data sources
Upstream records
- OSV (GitHub advisory)LoadedRaw JSON
- GitHub Advisory DatabaseLoadedRaw JSON
- OSV (CVE record)LoadedRaw JSON
- CVE.orgLoaded
- CISA KEV catalogLoaded
Fetched 2 days ago (12 Oct 2026, 07:49 UTC). Records refresh automatically after 24 hours.
Where each part of this page came from
- Title and description
- OSV (GitHub advisory)
- Severity score
- GitHub Advisory Database
- Weakness (CWE)
- GitHub Advisory Database
- Affected packages
- OSV (GitHub advisory)
- Fix
- OSV (GitHub advisory)
- References
- Computed by CVEminer
- SSVC
- CVE.org
- KEV status
- CISA KEV catalog
- EPSS
- GitHub Advisory Database
OSV and GitHub Advisory Database content is licensed CC BY 4.0. Values marked as computed or derived were calculated by CVEminer from the upstream data.