curl · Report #4112709
CVE-2026-111482: libssh2 knownhosts prefix match
How it was found
AI Disclaimer:
I've written this report with the help of AI, please feel free to let me know if I made a mistake, I've read the final revision of this report a few times to ensure it was solid but my understanding of the inner workings of curl and libssh is where I can fall short.
Summary:
ssh_force_knownhost_key_type() in lib/vssh/libssh2.c compares a [name]:port known_hosts entry, the form ssh writes for non-default ports, with strncmp(store->name + 1, conn->origin->hostname, kh_name_size). kh_name_size is the length of the name in the entry, so this is a prefix match: [10.0.0.1]:2222 also matches 10.0.0.12, and [git.example.co]:2222 also matches git.example.com. If such an entry for another host comes before the host's own entry with a different key type, curl forces that other key type for the connection. An attacker's server simply offers that type. The later check in ssh_knownhost() then finds no entry of that type for the host: with libssh2 1.11.1, libssh2_knownhost_checkp() only reports MISMATCH for a key of the same type. CURLOPT_SSH_KEYFUNCTION is therefore told CURLKHMATCH_MISSING instead of CURLKHMATCH_MISMATCH, which is the outcome of CVE-2026-87865, reached through plain entries instead of hashed ones; the code comes from 272282a054 (curl 7.69.0).
I would have liked to report this sooner, before 8.23.0 was out, but I've only managed to finish this now and I still have a lot of other curl CVEs to fix for the Debian package.
Affected version
Current master, 9a6d4ffc1 (2026-10-13), debug build on Debian forky/sid x86_64, with libssh2 1.11.1. The same code has been there since 7.69.0.
curl 8.23.0-DEV (x86_64-pc-linux-gnu) libcurl/8.23.0-DEV OpenSSL/3.6.5 zlib/1.3.2 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 libssh2/1.11.1 nghttp2/1.70.0 OpenLDAP/2.6.14
Release-Date: [unreleased]
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt mqtts pop3 pop3s rtsp scp sftp smtp smtps telnet tftp ws wss
Features: alt-svc AsynchDNS brotli Debug HSTS HTTP2 HTTPS-proxy IDN IPv6 Largefile libz SSL threadsafe UnixSockets zstd
Steps To Reproduce:
Copy the attached test3990.pl to tests/libtest/, and test3990 and test3991 to tests/data/. test3990 runs against the test suite's SFTP server, which has an RSA host key by default, with this known_hosts file:
[127.0.0]:PORT ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAqmXRY8k+W9GtpWxYru6ELAXYHdXqjf33ziITGTiJUN
[127.0.0.1]:PORT ssh-rsa <the server's real key>
cd tests && ./runtests.pl 3990 3991
test3990 fails, and its trace shows curl taking the [127.0.0] entry for 127.0.0.1:
SSH: found host '127.0.0.1' in 'log/known3990'
SSH: set 'ssh-ed25519' as hostkey type
Failure establishing ssh session: -5, Unable to exchange encryption keys
The handshake fails only because the test server has no ed25519 key; a man-in-the-middle server would offer one. test3991 is the control. Its first entry is for [127.0.0.1.2]:PORT, which is not a prefix of the host, so curl forces ssh-rsa and the transfer works.
Impact
Summary:
An attacker who can intercept connections can target a host whose name extends an earlier [name]:port entry with the same port, which is most likely with IP addresses such as 10.0.0.1 and 10.0.0.12. An application whose CURLOPT_SSH_KEYFUNCTION callback trusts new hosts would then accept the attacker's key instead of rejecting a mismatch, and authenticate to the attacker. The conditions are otherwise those of CVE-2026-87865: the libssh2 backend and an application-provided key callback. Without an attacker it is also a functional bug. A legitimate connection to such a host fails, either at key exchange or because its key is reported as missing, as in the test above.