{"id":"GHSA-rw4j-r22c-9gc3","summary":"AsyncSSH: asyncio event-loop freeze via SSH maximum packet size = 0 in SSH_MSG_CHANNEL_OPEN / OPEN_CONFIRMATION","details":"## Summary\n\nA malicious SSH server can wedge an AsyncSSH **client**, and an authenticated\nclient can wedge an AsyncSSH **server**, by sending a channel `maximum packet\nsize` of `0` in `SSH_MSG_CHANNEL_OPEN_CONFIRMATION` (server→client) or\n`SSH_MSG_CHANNEL_OPEN` (client→server). AsyncSSH stores the peer-supplied value\nverbatim with no lower-bound check; the first time channel data is written,\n`SSHChannel._flush_send_buf` enters a **synchronous infinite loop** that cannot\nbe interrupted by `asyncio.wait_for` or any timeout. The loop body has no\n`await`, so it blocks the entire asyncio event loop — for a server, one\nmalicious authenticated channel freezes **all** current and future connections.\n\nRFC 4254 §5.1 leaves receiver behavior for a peer-reported \"maximum packet\nsize = 0\" undefined, so the value must be rejected rather than stored.\n\n## Root cause\n\n`asyncssh/channel.py`:\n\n```python\n# process_open (server side)        -- line 465\nself._send_pktsize = send_pktsize    # peer value, no \u003e= 1 check\n\n# process_open_confirmation (client) -- line 528\nself._send_pktsize = send_pktsize    # peer value, no \u003e= 1 check\n\n# _flush_send_buf                    -- lines 305-320\nwhile self._send_buf and self._send_window:\n    pktsize = min(self._send_window, self._send_pktsize)  # 0 when peer sends 0\n    buf, datatype = self._send_buf[0]\n    if len(buf) \u003e pktsize:          # True for any buffered data\n        data = buf[:pktsize]        # empty (b'')\n        del buf[:pktsize]           # no-op\n    ...\n    self._send_window -= len(data)  # -= 0, unchanged\n```\n\nWith `_send_pktsize == 0`, `pktsize` is `0`, so `buf[:0]` is empty,\n`del buf[:0]` is a no-op, and `_send_window` is never decremented — the\n`while` condition is permanently true, and with no `await` in the body the\nevent loop is blocked.\n\n## Impact\n\n- **Client vector (primary):** a malicious SSH server replies to the client's\n  channel open with `maximum packet size = 0`; the client wedges on its first\n  channel write. The attacker is the server, so it needs no valid credentials.\n- **Server vector:** an authenticated client opens a channel with\n  `maximum packet size = 0`; any server-side channel write wedges the AsyncSSH\n  server's event loop, freezing **every** current and future connection. A\n  single low-privilege account can take the whole server down.\n\nBoth vectors are a single SSH message, deterministic, and cause total\navailability loss for the affected process.\n\n## Affected versions\n\n\u003c= 2.23.1 (latest release, 2026-06-06); also present on `master`\n(channel.py:465/528 unguarded). Verified end-to-end on 2.23.1.\n\n## Verification\n\nThe maintainer's proposed fix (reject `send_pktsize == 0` in `connection.py`\n`_process_channel_open` / `_process_channel_open_confirmation`) was applied to\n2.23.1 and re-tested end-to-end over TCP:\n\n- **Unpatched:** malicious server (paramiko forcing `max_packet_size=0` in\n  OPEN_CONFIRMATION) + real asyncssh client → client event loop wedges.\n- **Patched:** the guard fires inside `_process_channel_open_confirmation`, the\n  malicious value is rejected, the connection closes cleanly\n  (`ChannelOpenError: SSH connection closed`), and the client does **not** wedge.\n\nThe maintainer (Ron Frederick) independently confirmed the freeze and noted that\n**even shutting the server down does not break clients out of the loop**.\n\nReproducers available: a focused harness driving the real\n`SSHChannel._flush_send_buf` with `_send_pktsize=0`, and an end-to-end\n`malicious_server.py` (paramiko) + `client.py` (real asyncssh) pair. The\nend-to-end client repro uses `asyncio.new_event_loop()` (not `get_event_loop()`)\nfor Python 3.14 compatibility.\n\n## Suggested fix (maintainer's approach)\n\nIn `connection.py`, after each `send_pktsize = packet.get_uint32()` in\n`_process_channel_open` and `_process_channel_open_confirmation`:\n\n```python\nif send_pktsize == 0:\n    raise ProtocolError('Invalid maximum packet size')\n```\n\n## CVSS\n\n`CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H` (6.5 Medium). An earlier draft\nquoted 7.5 High (\"100% CPU\"); the synchronous loop burns ~100% of one core's\nworth of CPU but, being single-threaded, the OS scheduler spreads it across\ncores, so the real impact is event-loop / connection freeze, not machine-wide\nCPU exhaustion.\n\n## References\n\n- RFC 4254 §5.1 (channel \"maximum packet size\"; behavior for 0 is undefined).\n- The same `maximum packet size = 0` send-loop wedge was confirmed in several\n  other independent SSH implementations (different languages/runtimes) and\n  reported to each maintainer separately.\n\n## Credits\n\nReported by zhangph (afldl), 2026-06-20.\n```","aliases":["CVE-2026-62949"],"modified":"2026-09-17T15:00:05.161759678Z","published":"2026-09-17T14:53:01Z","database_specific":{"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-09-17T14:53:01Z","nvd_published_at":"2026-09-16T20:17:26Z","cwe_ids":["CWE-835"]},"references":[{"type":"WEB","url":"https://github.com/ronf/asyncssh/security/advisories/GHSA-rw4j-r22c-9gc3"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-62949"},{"type":"WEB","url":"https://github.com/ronf/asyncssh/commit/756cbae5350789ce9735f15f704bae9b5a3608b8"},{"type":"WEB","url":"https://github.com/ronf/asyncssh/commit/9c354270c009285525e126721e8ed5fbed1f8a67"},{"type":"PACKAGE","url":"https://github.com/ronf/asyncssh"},{"type":"WEB","url":"https://github.com/ronf/asyncssh/releases/tag/v2.24.0"}],"affected":[{"package":{"name":"asyncssh","ecosystem":"PyPI","purl":"pkg:pypi/asyncssh"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.24.0"}]}],"versions":["0.8.1","0.8.2","0.8.3","0.8.4","0.9.0","0.9.1","0.9.2","1.0.0","1.0.1","1.1.0","1.1.1","1.10.0","1.10.1","1.11.0","1.11.1","1.12.0","1.12.1","1.12.2","1.13.0","1.13.1","1.13.2","1.13.3","1.14.0","1.15.0","1.15.1","1.16.0","1.16.1","1.17.0","1.17.1","1.18.0","1.2.0","1.2.1","1.3.0","1.3.1","1.3.2","1.4.0","1.4.1","1.5.0","1.5.1","1.5.2","1.5.3","1.5.4","1.5.5","1.5.6","1.6.0","1.6.1","1.6.2","1.7.1","1.7.2","1.7.3","1.8.0","1.8.1","1.9.0","2.0.0","2.0.1","2.1.0","2.10.0","2.10.1","2.11.0","2.12.0","2.13.0","2.13.1","2.13.2","2.14.0","2.14.1","2.14.2","2.15.0","2.16.0","2.17.0","2.18.0","2.19.0","2.2.0","2.2.1","2.20.0","2.21.0","2.21.1","2.22.0","2.23.0","2.23.1","2.3.0","2.4.0","2.4.1","2.4.2","2.5.0","2.6.0","2.7.0","2.7.1","2.7.2","2.8.0","2.8.1","2.9.0"],"database_specific":{"last_known_affected_version_range":"\u003c= 2.23.1","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-rw4j-r22c-9gc3/GHSA-rw4j-r22c-9gc3.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H"}]}