Resumen
El fallo está en ssh_force_knownhost_key_type(), en lib/vssh/libssh2.c, la función de curl que elige el tipo de clave de host que se va a exigir en la conexión SSH a partir del fichero known_hosts. Cuando el servidor usa un puerto distinto del predeterminado, ssh escribe la entrada con la forma [nombre]:puerto, y curl la compara así:
strncmp(store->name + 1, conn->origin->hostname, kh_name_size)
kh_name_size es la longitud del nombre que aparece en la entrada, no la del host al que se conecta curl. Por eso la comparación solo comprueba que la entrada sea un prefijo del host, no que coincidan los dos nombres:
[10.0.0.1]:2222también coincide con10.0.0.12.[git.example.co]:2222también coincide congit.example.com.
La consecuencia encadena tres pasos. Si una entrada de otro host queda en el fichero antes que la entrada propia y tiene un tipo de clave distinto, curl fuerza ese otro tipo para la conexión. Un servidor atacante solo tiene que ofrecer ese tipo. Después, la comprobación de ssh_knownhost() no encuentra ninguna entrada de ese tipo para el host, porque con libssh2 1.11.1 libssh2_knownhost_checkp() solo informa de MISMATCH si la clave es del mismo tipo. El resultado es que el callback CURLOPT_SSH_KEYFUNCTION recibe CURLKHMATCH_MISSING en lugar de CURLKHMATCH_MISMATCH.
Es el mismo desenlace de CVE-2026-87865, pero alcanzado con entradas en claro en vez de entradas con hash. El código procede del commit 272282a054, incluido en curl 7.69.0.
El investigador lo comprobó sobre master en el commit 9a6d4ffc1 (13 de octubre de 2026), con una compilación de depuración en Debian forky/sid x86_64 y libssh2 1.11.1:
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
Los datos del reporte: enviado el 14 de octubre de 2026, divulgado el 26 de octubre y marcado como resuelto, con severidad baja. El autor avisa de que lo escribió con ayuda de IA y de que revisó el texto final varias veces.
Pasos de reproducción
La prueba se apoya en dos casos de la propia batería de tests de curl, adjuntos al reporte original.
- Copia
test3990.plentests/libtest/. - Copia
test3990ytest3991entests/data/. - El caso
test3990se ejecuta contra el servidor SFTP de la batería de tests, que por defecto tiene una clave de host RSA, usando esteknown_hosts:
[127.0.0]:PORT ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAqmXRY8k+W9GtpWxYru6ELAXYHdXqjf33ziITGTiJUN
[127.0.0.1]:PORT ssh-rsa <the server's real key>
- Ejecuta los dos casos:
cd tests && ./runtests.pl 3990 3991
test3990falla, y la traza muestra que curl ha usado la entrada[127.0.0]para conectar a127.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
La negociación falla únicamente porque el servidor de pruebas no tiene clave ed25519; un servidor intermedio malicioso ofrecería una.
test3991sirve de control. Su primera entrada es para[127.0.0.1.2]:PORT, que no es prefijo del host, así que curl fuerzassh-rsay la transferencia funciona.
Impacto
El ataque exige poder interceptar conexiones y que el host objetivo tenga un nombre que prolongue el de una entrada [nombre]:puerto anterior con el mismo puerto. Lo más probable es que ocurra con direcciones IP, como 10.0.0.1 y 10.0.0.12.
Si además la aplicación tiene un callback CURLOPT_SSH_KEYFUNCTION que confía en los hosts nuevos, aceptará la clave del atacante en vez de rechazar la discrepancia, y se autenticará contra él. El resto de condiciones son las de CVE-2026-87865: backend libssh2 y callback de clave aportado por la aplicación.
Sin atacante también hay un fallo funcional: una conexión legítima a ese host se rompe, bien en el intercambio de claves, bien porque su clave se da por ausente, como en la prueba anterior.
Qué aprender de este caso
- Desconfía de cualquier
strncmpcuya longitud salga del valor guardado y no de los dos valores comparados: es una comparación de prefijo. Para exigir igualdad hay que comprobar también que las longitudes coinciden. - Al revisar parsers de formatos con delimitadores, como
[host]:puerto, mira qué pasa justo después del nombre extraído. Probar valores que prolongan uno ya guardado (10.0.0.1frente a10.0.0.12,example.cofrente aexample.com) destapa este error enseguida. - Vigila los códigos que distinguen "clave cambiada" de "host desconocido". Si un atacante puede pasar del primero al segundo, cualquier política de confiar en hosts nuevos se convierte en una vía para saltarse la verificación.
- Cuando se corrige un CVE, busca otras rutas que lleven al mismo resultado: aquí el desenlace de CVE-2026-87865 se alcanzaba con entradas en claro en lugar de con hash.