libssh2 CVE-2026-55200 benign PoC
Benign local proof of concept for CVE-2026-55200, an unchecked SSH packet_length condition in libssh2's ssh2_transport_read() transport parser path.
Research status: verified local arithmetic and state model.
Summary
libssh2 through 1.11.1 accepted an attacker-controlled SSH packet length in one full-packet decryption path without first enforcing the RFC-sized libssh2 packet maximum.
The vulnerable shape is:
total_num = 4
packet_length = decoded SSH packet_length field
reject only if packet_length < 1
total_num += packet_length + mac_len + auth_len
reject if total_num > 35000 or total_num == 0
allocate total_num bytes
On a build where size_t is 32-bit, a packet length of 0xffffffff with auth_len=16 wraps the computed allocation size to 19 bytes:
4 + 0xffffffff + 0 + 16 == 19 modulo 2^32
The original packet_length remains 0xffffffff. Later full-packet processing can still use packet-length-derived sizes, including a packet_length - 1 style length, after the allocation decision has already been made.
The upstream fix rejects packet_length > LIBSSH2_PACKET_MAXPAYLOAD before the addition.
This PoC does not generate SSH traffic, does not attempt remote exploitation, and does not perform an out-of-bounds write. It is a standalone verifier for the arithmetic and decision-state transition.
Files
poc/cve_2026_55200_probe.c- standalone C11 benign verifier with no source commentsevidence/2026-06-23-local-harness-output.txt- local build and replay evidence
Affected Source Path
The affected path is in:
src/transport.c:ssh2_transport_read()
The fix is upstream commit:
97acf3dfda80c91c3a8c9f2372546301d4a1a7a8
The relevant fix adds a packet_length > LIBSSH2_PACKET_MAXPAYLOAD guard before the vulnerable addition in the full-packet path.
Build and Run
Linux, macOS, WSL, or MinGW:
gcc -std=c11 -Wall -Wextra -O0 -g -o cve_2026_55200_probe poc/cve_2026_55200_probe.c
./cve_2026_55200_probe
Windows PowerShell with MinGW:
gcc -std=c11 -Wall -Wextra -O0 -g -o cve_2026_55200_probe.exe .\poc\cve_2026_55200_probe.c
.\cve_2026_55200_probe.exe
No arguments defaults to the benign proof mode. The same mode can be selected explicitly:
./cve_2026_55200_probe --benign
Additional diagnostic modes:
./cve_2026_55200_probe --native
./cve_2026_55200_probe --check
The input values can be overridden:
./cve_2026_55200_probe --packet-length 0xffffffff --mac-len 0 --auth-len 16
Expected Output
On a 64-bit build, the default benign run should report result=PASS:
benign CVE-2026-55200 proof
build_size_t_bytes=8
build_size_t_bits=64
packet_length=0xffffffff (4294967295)
mac_len=0
auth_len=16
mathematical_total=4294967315
vulnerable32_decision=accepted
vulnerable32_total=19
vulnerable32_allocation=19
fullpacket_style_length=4294967294
allocation_gap=4294967275
fixed32_decision=rejected: out of boundary
native_unpatched_decision=rejected: out of boundary
native_unpatched_total=4294967315
native_note=64-bit native arithmetic rejects this default input; modeled 32-bit arithmetic remains vulnerable
result=PASS
The --native mode shows what the current binary's actual size_t arithmetic does. On a 64-bit build, native arithmetic rejects the default input because the computed total does not wrap before the existing total_num > 35000 check:
native-size_t check
build_size_t_bytes=8
build_size_t_bits=64
unpatched_decision=rejected: out of boundary
unpatched_total=4294967315
unpatched_allocation=0
fixed_decision=rejected: out of boundary
fixed_total=0
fixed_allocation=0
Mechanics
The verifier computes three decisions for the same input:
vulnerable32: the vulnerable arithmetic using a 32-bitsize_tstand-infixed32: the patched 32-bit decision with the packet-length maximum checknative_unpatched: the unpatched decision using the current binary's realsize_t
For the default input, the modeled 32-bit vulnerable path accepts the packet and derives a 19-byte allocation. The fixed model rejects before allocation. A native 64-bit build also rejects the default input, but that does not invalidate the 32-bit vulnerable state; it shows the architecture dependency explicitly.
The proof condition is:
vulnerable32_decision == accepted
vulnerable32_allocation == 19
packet_length > 35000
fullpacket_style_length > vulnerable32_allocation
fixed32_decision == rejected
Why This Is Benign
The program does not connect to any host, bind any socket, generate any SSH packet, or copy past an allocation. It only models the arithmetic and branch decisions needed to distinguish the vulnerable and fixed behavior.
The allocation_gap value is a diagnostic showing how far a later packet-length-derived operation would exceed the modeled allocation if such an operation were performed.
References
- CVE record:
https://www.cve.org/CVERecord?id=CVE-2026-55200 - NVD entry:
https://nvd.nist.gov/vuln/detail/CVE-2026-55200 - Upstream fix:
https://github.com/libssh2/libssh2/commit/97acf3dfda80c91c3a8c9f2372546301d4a1a7a8 - Upstream PR:
https://github.com/libssh2/libssh2/pull/2052 - VulnCheck advisory:
https://www.vulncheck.com/advisories/libssh2-out-of-bounds-write-via-unchecked-packet-length-in-transport-c
Responsible Use
Run this PoC only as a local research and regression-verification harness. It is intentionally scoped to a non-network arithmetic model.