A buffer waiting to receive an SSH packet trusts the attacker's stated packet length without sanity-checking it
libssh2's ssh2_transport_read() in transport.c reads the 4-byte packet_length field straight off the wire and uses it to size buffers and drive copy logic without an upper-bound check. A malicious peer supplies a huge value, an integer math step wraps, and the library performs an out-of-bounds heap write during the very first transport negotiation — *before* key exchange, *before* authentication, *before* the user types anything. This is a classic CWE-680 (integer overflow to buffer overflow) primitive, and with the right heap shaping it gives remote code execution in the process embedding libssh2. All versions through 1.11.1 are affected; the fix landed in upstream commit 7acf3df.
The vendor's CRITICAL/9.2 rating is honest for the *primitive* — pre-auth, network-reachable memory corruption is exactly what 9.x is supposed to mean. What the CVSS doesn't show is the direction of the arrow: libssh2 is overwhelmingly used as a client library (curl --ssh, PHP's ssh2 extension, libgit2 SSH transport, some backup agents, embedded device management clients), so the victim has to initiate the connection — to either an attacker-controlled SSH server or an on-path attacker who can intercept TCP/22. That's why we hold this at HIGH (8.2) instead of CRITICAL: the bug is real, the impact is real, but the *trigger* requires the attacker to be on the path or to lure the client. Reliability is also non-trivial — the CVSS 4.0 vector explicitly carries AT:P, meaning heap-layout conditions must hold for code execution.
4 steps from start to impact.
Position as a malicious SSH endpoint
KEXINIT packet. Alternatively, they MitM an existing client→server SSH flow on an untrusted network segment (open Wi-Fi, compromised ISP, BGP hijack, rogue jump host). No credentials, no certificates, nothing — the attacker just needs the TCP socket.- Attacker controls a server the victim connects to, OR sits on the network path
- Victim runs a libssh2-based client ≤ 1.11.1
- Inbound clients to attacker servers require luring (typo-squat git mirrors, phishing repo URLs, malicious
known_hostsentries) - On-path MitM of port 22 inside enterprise networks is non-trivial behind segmented VLANs and zero-trust egress
Send oversized packet_length during transport read
ssh2_transport_read() parses the 4-byte length prefix of the attacker's SSH binary packet. The attacker sends a value crafted to wrap during the buffer-size arithmetic — e.g., near 0xFFFFFFFF — so the resulting allocation is small but the subsequent copy walks far past its end. Weaponization: a ~50-line Python PoC using socket is sufficient to demonstrate the crash; expect public PoCs within days.- TCP handshake completed
- Victim libssh2 calls
_libssh2_transport_readon attacker bytes
- None at this stage — the bug is pre-auth and pre-kex
packet_length > MAX_SSH_PACKET (35000) are trivial to write but not deployed by defaultShape the heap for write-what-where
_libssh2_session struct field lands next to the corrupted chunk. Tooling: standard heap-grooming techniques from house-of-* family; researchers like *Tristan Madani* (credited reporter) have not yet published a full chain.- Ability to send multiple packets before crash
- Knowledge of target libc / allocator / ASLR posture
- ASLR + PIE on modern Linux distros
- Hardened allocators (scudo on Android, GWP-ASan)
- Single-shot clients (e.g., a one-off
git clone) give few grooming opportunities
Achieve RCE in the embedding process
ssh2_connect(). From there, standard post-exploitation — credential theft, lateral movement, supply-chain pivot if the popped host is a build node.- Successful step 3
- RELRO/Fortify/CFI in modern binaries reduces gadget viability
- Sandboxed clients (firejail, seccomp) limit damage
The supporting signals.
| In-the-wild exploitation | None observed as of 2026-06-23. No KEV entry, no incident reports, no honeypot telemetry. |
|---|---|
| Public PoC | No public weaponized exploit yet. Researcher *Tristan Madani* published the advisory but withheld a chain. Expect crash-PoCs within 1-2 weeks. |
| EPSS | 0.00552 (~0.55%) — low, consistent with no public exploit and client-side trigger model |
| KEV status | Not listed by CISA as of 2026-06-23 |
| CVSS vector | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N → 9.2. Note AT:P — attack requirements present (heap grooming for full RCE) |
| Affected versions | libssh2 through 1.11.1 (all currently shipped releases at disclosure) |
| Fixed version | Upstream commit 7acf3df. Distro backports: watch RHEL libssh2, Debian/Ubuntu libssh2-1, Alpine libssh2, Amazon Linux libssh2. Also rebuild downstream consumers — curl, libgit2, PHP ssh2, php-ssh2. |
| Exposure (Shodan/Censys) | Not directly scannable — libssh2 is a client library; it does not expose a listening port. Inventory must come from SBOM / package manager, not network scans. |
| Disclosure date | 2026-06-17 |
| Reporter | Tristan Madani (independent researcher) |
noisgate verdict.
Downgrading from CRITICAL/9.2 to HIGH/8.2 because the single most decisive factor is the *direction of the connection*: libssh2 is a client library, so the attacker must lure the victim to a malicious server or sit on-path — there is no unsolicited internet-facing trigger. The bug stays in HIGH (not lower) because the affected component is embedded in build agents, backup tooling, and PHP/curl-based automation where a single popped host pivots into supply-chain or fleet-scale impact.
Why this verdict
- Client-side trigger, not server-side: libssh2 listens for nothing; the victim must initiate the SSH session. That removes the "internet-wide spray" failure mode that defines true CRITICAL network bugs like Log4Shell or BlueKeep.
AT:Pis real: CVSS 4.0 explicitly flags attack requirements. Crashing the client is trivial; achieving RCE requires per-target heap grooming against modern allocators with ASLR.- No PoC, no KEV, EPSS 0.55%: none of the three independent exploitation indicators is firing. Vendor 9.2 reflects the primitive's theoretical ceiling, not the realistic near-term threat.
- Role multiplier — CI/CD and build agents: Jenkins/GitLab runners using
libgit2+libssh2 to clone from SSH remotes are a canonical supply-chain pivot. A poisoned mirror or hijacked DNS forgit.internallands code execution on the builder, which then signs and publishes artifacts. This is why the floor is HIGH, not MEDIUM. - Role multiplier — backup and replication agents: Veeam-style agents, rsync-over-ssh wrappers, and storage replication tools that embed libssh2 typically run as root/SYSTEM and connect to many endpoints. Any one compromised remote pops the backup tier — fleet-scale blast radius.
- Role multiplier — PHP
ssh2extension on web tier: if a web app callsssh2_connect()against attacker-controllable hostnames (SSRF-adjacent feature), the web process pops. Narrow but real.
Why not higher?
CRITICAL is reserved for bugs an attacker can ring without invitation — listening services, browser drive-bys, or active mass scanning. This bug requires either victim-initiated connection to a hostile endpoint or on-path interception, neither of which scales like a wormable server-side flaw. No public exploit and EPSS under 1% confirm we are not yet in fire-drill territory.
Why not lower?
MEDIUM would be wrong because the bug is pre-authentication memory corruption in a library shipped in *thousands* of distinct products, including high-value-role components (build agents, backup tooling). A single supply-chain compromise of a popular git mirror combined with this CVE compromises every CI runner that clones it. That blast radius floors the verdict at HIGH per noisgate role-multiplier rules.
What to do — in priority order.
- Inventory every libssh2 consumer via SBOM — You cannot scan for this — libssh2 doesn't listen. Pull from package managers (
dpkg -l libssh2-1,rpm -qa | grep libssh2,apk info libssh2), language ecosystems (PHPpecl list, Python packages bundling it, Node native modules), and container image SBOMs (Syft/Grype). Complete within 7 days under the HIGH noisgate mitigation SLA (≤ 30 days). - Restrict outbound SSH to allowlisted endpoints — Force all egress SSH through known jump hosts or a managed proxy. Block direct port-22 egress from CI runners, web tiers, and backup agents to anywhere outside the allowlist. Deploy within the noisgate mitigation SLA of 30 days — this neutralizes the malicious-server vector for managed networks.
- Pin git/clone sources to verified internal mirrors — Build pipelines must clone only from your internal artifact registry or mirrored Git server, never from arbitrary upstream URLs. This breaks the supply-chain attack path even if libssh2 is unpatched on the runner.
- Enable verbose
known_hostschecking and pin host keys — Strict host-key checking ensures an on-path MitM presenting a different SSH server key terminates the connection beforessh2_transport_read()is called against attacker bytes. ConfigureStrictHostKeyChecking=yesand pre-seedknown_hosts. - Rebuild and ship downstream consumers — Patching libssh2 the package is not enough if you ship containers or appliances that statically link or bundle older builds. Rebuild every internal image whose SBOM lists libssh2 ≤ 1.11.1 and push within the noisgate remediation SLA of 180 days.
- Network IDS signatures alone — outbound SSH is opaque after the first packet; you cannot inspect content. The malicious length field is sent in cleartext for ~1 packet, but most IDS deployments do not parse SSH binary packets.
- WAF rules — irrelevant; libssh2 is not behind a WAF, and HTTP-layer controls don't see SSH.
- Disabling password auth / requiring MFA on SSH — the bug fires *before* authentication. Auth hardening on the server side has zero effect on a vulnerable client.
- Patching only
openssh-client— OpenSSH and libssh2 are independent codebases. The libssh2 fix is in a separate package; anopensshupdate does not address this CVE.
Crowdsourced verification payload.
Run this on each Linux/macOS host suspected of using libssh2 — including build runners, web servers, backup agents. Invoke as sudo ./check_libssh2.sh (root required to read all linked binaries). It enumerates package versions, ldconfig entries, and statically embedded copies, then prints VULNERABLE / PATCHED / UNKNOWN.
#!/usr/bin/env bash
# noisgate: CVE-2026-55200 libssh2 OOB write verifier
# Exit codes: 0 PATCHED, 1 VULNERABLE, 2 UNKNOWN
set -u
FIXED_VER="1.11.2" # first release expected to carry commit 7acf3df
VULN=0
FOUND=0
compare_ver() {
# returns 0 if $1 >= $2
[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" = "$2" ]
}
echo "[*] Checking package manager records..."
if command -v dpkg >/dev/null 2>&1; then
while read -r name ver; do
[ -z "$name" ] && continue
FOUND=1
clean=$(echo "$ver" | sed 's/[-+].*//')
if compare_ver "$clean" "$FIXED_VER"; then
echo " [OK] dpkg $name $ver"
else
echo " [!!] dpkg $name $ver <- below $FIXED_VER"
VULN=1
fi
done < <(dpkg-query -W -f='${Package} ${Version}\n' 2>/dev/null | grep -E '^libssh2')
fi
if command -v rpm >/dev/null 2>&1; then
while read -r line; do
[ -z "$line" ] && continue
FOUND=1
name=$(echo "$line" | awk '{print $1}')
ver=$(echo "$line" | awk '{print $2}')
if compare_ver "$ver" "$FIXED_VER"; then
echo " [OK] rpm $name $ver"
else
echo " [!!] rpm $name $ver <- below $FIXED_VER (check distro backport)"
VULN=1
fi
done < <(rpm -qa --qf '%{NAME} %{VERSION}\n' 2>/dev/null | grep -E '^libssh2')
fi
if command -v apk >/dev/null 2>&1; then
while read -r line; do
[ -z "$line" ] && continue
FOUND=1
pkg=$(echo "$line" | sed 's/-[0-9].*//')
ver=$(echo "$line" | sed "s/^${pkg}-//")
if compare_ver "$ver" "$FIXED_VER"; then
echo " [OK] apk $pkg $ver"
else
echo " [!!] apk $pkg $ver"
VULN=1
fi
done < <(apk info -v 2>/dev/null | grep -E '^libssh2')
fi
echo "[*] Scanning linked shared objects..."
for so in $(find /usr /opt /app -name 'libssh2.so*' 2>/dev/null); do
FOUND=1
ver=$(strings "$so" 2>/dev/null | grep -oE 'libssh2/[0-9]+\.[0-9]+\.[0-9]+' | head -n1 | sed 's|libssh2/||')
if [ -z "$ver" ]; then
echo " [??] $so (no embedded version string)"
continue
fi
if compare_ver "$ver" "$FIXED_VER"; then
echo " [OK] $so -> $ver"
else
echo " [!!] $so -> $ver"
VULN=1
fi
done
echo "[*] Hunting for statically linked copies in common binaries..."
for b in $(command -v curl git php node python3 2>/dev/null); do
if strings "$b" 2>/dev/null | grep -q 'libssh2/'; then
FOUND=1
sver=$(strings "$b" | grep -oE 'libssh2/[0-9]+\.[0-9]+\.[0-9]+' | head -n1 | sed 's|libssh2/||')
if [ -n "$sver" ] && compare_ver "$sver" "$FIXED_VER"; then
echo " [OK] $b statically embeds libssh2 $sver"
else
echo " [!!] $b statically embeds libssh2 ${sver:-unknown}"
VULN=1
fi
fi
done
if [ "$FOUND" -eq 0 ]; then
echo "RESULT: UNKNOWN (no libssh2 evidence found on this host)"
exit 2
fi
if [ "$VULN" -eq 1 ]; then
echo "RESULT: VULNERABLE"
exit 1
fi
echo "RESULT: PATCHED"
exit 0
If you remember one thing.
libssh2 ≤ 1.11.1. You cannot find this with a network scanner — it has to come from package inventory. Under the noisgate mitigation SLA for HIGH (≤ 30 days), block direct outbound SSH from servers to non-allowlisted endpoints and pin Git clone sources to internal mirrors; that alone neutralizes the realistic supply-chain pivot. Under the noisgate remediation SLA for HIGH (≤ 180 days), roll the distro update once your vendor (RHEL, Ubuntu, Alpine, Amazon Linux) publishes a backport and rebuild every container image that bundles libssh2 statically. If a public RCE PoC drops or this lands in KEV, override both deadlines and patch/mitigate within hours — the *primitive* is genuinely 9.2, only the trigger model holds it back.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.