Resumen
curl puede usar dos bibliotecas para SFTP/SCP: libssh2 y libssh. Este fallo, registrado como CVE-2026-9547, afecta solo al backend de libssh, en lib/vssh/libssh.c, y está en cómo curl traduce el resultado de la comprobación de la clave del servidor a los valores que recibe la aplicación.
libssh distingue varios resultados al comparar la clave del servidor con known_hosts. Uno de ellos es SSH_KNOWN_HOSTS_OTHER, que la propia cabecera de libssh documenta así:
/* The server gave us a key of a type while we had an other type recorded.
It is a possible attack. */
SSH_KNOWN_HOSTS_OTHER = -2,
Es decir: el host ya está en known_hosts, pero con una clave de otro tipo (por ejemplo, RSA) y el servidor presenta ahora una de un tipo distinto (por ejemplo, Ed25519). Sin embargo, curl lo agrupaba con los casos de "primer contacto" y se lo comunicaba a la aplicación como CURLKHMATCH_MISSING, en lugar de CURLKHMATCH_MISMATCH:
case SSH_KNOWN_HOSTS_OK:
keymatch = CURLKHMATCH_OK; break;
case SSH_KNOWN_HOSTS_OTHER: /* downgraded to MISSING */
case SSH_KNOWN_HOSTS_NOT_FOUND:
case SSH_KNOWN_HOSTS_UNKNOWN:
case SSH_KNOWN_HOSTS_ERROR:
keymatch = CURLKHMATCH_MISSING; break;
default: /* SSH_KNOWN_HOSTS_CHANGED */
keymatch = CURLKHMATCH_MISMATCH; break;
Según el investigador, el fallo viene del commit 507cf6a13d (21-feb-2020, "libssh: improve known hosts handling"), que pasó la verificación a la API ssh_session_is_known_server() y colocó SSH_KNOWN_HOSTS_OTHER en ese grupo en vez de dejarlo caer en la rama por defecto (CURLKHMATCH_MISMATCH). El backend libssh2 no tiene un estado equivalente y no está afectado.
Cuándo devuelve libssh SSH_KNOWN_HOSTS_OTHER
La lógica está en ssh_known_hosts_check_server_key() (src/knownhosts.c de libssh). Simplificada:
found = SSH_KNOWN_HOSTS_UNKNOWN;
for each known_hosts entry matching this host:
if(server_key == entry_key)
return SSH_KNOWN_HOSTS_OK;
if(ssh_key_type(server_key) == ssh_key_type(entry_key)) {
found = SSH_KNOWN_HOSTS_CHANGED;
continue;
}
if(found != SSH_KNOWN_HOSTS_CHANGED)
found = SSH_KNOWN_HOSTS_OTHER;
return found;
Aplicado a casos concretos:
known_hostssolo tiene RSA y el servidor presenta la misma clave RSA →SSH_KNOWN_HOSTS_OK.known_hostssolo tiene RSA y el servidor presenta otra clave RSA →SSH_KNOWN_HOSTS_CHANGED.known_hostssolo tiene RSA y el servidor presenta una clave Ed25519 →SSH_KNOWN_HOSTS_OTHER.known_hoststiene RSA y Ed25519 y el servidor presenta otra Ed25519 →SSH_KNOWN_HOSTS_CHANGED.known_hoststiene RSA y Ed25519 y el servidor presenta ECDSA →SSH_KNOWN_HOSTS_OTHER(salvo que también haya una entrada ECDSA distinta).
Un matiz: ssh-rsa, rsa-sha2-256 y rsa-sha2-512 son el mismo tipo base (RSA), así que una clave RSA distinta siempre da CHANGED, no OTHER. Y, como subraya el investigador, el problema de curl no depende de lo fácil que sea llegar a este estado: siempre que libssh devuelve SSH_KNOWN_HOSTS_OTHER, curl lo convierte en CURLKHMATCH_MISSING.
Pasos de reproducción
- Preparar un
sshdque solo sirva una clave de host Ed25519 (o configurado para que la clave negociada sea Ed25519), escuchando en127.0.0.1:2222. - Crear un fichero
known_hostsque, para ese host y puerto, contenga solo una entradassh-rsa, sin ninguna entrada Ed25519 en otros ficherosknown_hostsde usuario o globales. - Guardar como
poc.cun cliente con un callbackCURLOPT_SSH_KEYFUNCTIONde tipo TOFU (rechazaMISMATCH, acepta y guardaMISSING):
#include <stdio.h>
#include <curl/curl.h>
static int keyfunc(CURL *e, const struct curl_khkey *k,
const struct curl_khkey *f,
enum curl_khmatch m, void *p) {
(void)e; (void)k; (void)f; (void)p;
switch(m) {
case CURLKHMATCH_OK: fprintf(stderr, "OK\n"); return CURLKHSTAT_FINE;
case CURLKHMATCH_MISMATCH: fprintf(stderr, "MISMATCH\n"); return CURLKHSTAT_REJECT;
case CURLKHMATCH_MISSING: fprintf(stderr, "MISSING\n"); return CURLKHSTAT_FINE_ADD_TO_FILE;
default: fprintf(stderr, "?\n"); return CURLKHSTAT_REJECT;
}
}
int main(int argc, char **argv) {
curl_global_init(CURL_GLOBAL_DEFAULT);
CURL *e = curl_easy_init();
curl_easy_setopt(e, CURLOPT_URL, "sftp://127.0.0.1:2222/x");
curl_easy_setopt(e, CURLOPT_USERPWD, "u:p");
curl_easy_setopt(e, CURLOPT_SSH_KNOWNHOSTS, argv[1]);
curl_easy_setopt(e, CURLOPT_SSH_KEYFUNCTION, keyfunc);
curl_easy_setopt(e, CURLOPT_VERBOSE, 1L);
CURLcode rc = curl_easy_perform(e);
fprintf(stderr, "rc=%d (%s)\n", rc, curl_easy_strerror(rc));
curl_easy_cleanup(e);
curl_global_cleanup();
return 0;
}
- Compilar curl con libssh y enlazar la prueba contra la biblioteca estática:
autoreconf -fi
mkdir build && cd build
CFLAGS='-O1 -g' CC=clang \
../configure --with-openssl --with-libssh --enable-debug --disable-shared
make -j
clang -g -I../include poc.c ./lib/.libs/libcurl.a \
-lssl -lcrypto -lz -lnghttp2 -lidn2 -lzstd -lbrotlidec -lldap -llber -lssh \
-o poc
- Ejecutar
pocpasando como primer argumento la ruta delknown_hostsdel paso 2. Como libssh clasifica la situación comoSSH_KNOWN_HOSTS_OTHERy curl la traduce aCURLKHMATCH_MISSING, el callback la trata como un host nuevo en lugar de como un cambio de clave.
Como prueba a nivel de código, el investigador muestra dónde aparece la constante en ambos lados:
$ grep -n SSH_KNOWN_HOSTS_OTHER lib/vssh/libssh.c
279: case SSH_KNOWN_HOSTS_OTHER:
280: case SSH_KNOWN_HOSTS_NOT_FOUND:
$ grep -n SSH_KNOWN_HOSTS_OTHER /usr/include/libssh/libssh.h
254: /* The server gave us a key of a type while we had an other type recorded.
255: It is a possible attack. */
256: SSH_KNOWN_HOSTS_OTHER = -2,
Impacto
Afecta a las aplicaciones que usan curl con el backend libssh y un CURLOPT_SSH_KEYFUNCTION con lógica TOFU (aceptar y guardar los hosts nuevos, rechazar los que cambian de clave). Si un servidor ya conocido presenta una clave de un tipo que no está registrado para ese host, la aplicación recibe CURLKHMATCH_MISSING, la acepta en silencio y, con CURLKHSTAT_FINE_ADD_TO_FILE, la añade a known_hosts. Lo que libssh describe como "posible ataque" llega a la aplicación como un primer contacto normal, de modo que la protección de known_hosts queda debilitada: en palabras del investigador, cualquier conexión puede acabar siendo TOFU.
Remediación
El reporte figura como resuelto en HackerOne. La corrección propuesta por el investigador consiste en sacar SSH_KNOWN_HOSTS_OTHER del grupo de "desconocido" y hacer que devuelva CURLKHMATCH_MISMATCH:
--- a/lib/vssh/libssh.c
+++ b/lib/vssh/libssh.c
@@ -276,7 +276,9 @@
case SSH_KNOWN_HOSTS_OK:
keymatch = CURLKHMATCH_OK;
break;
- case SSH_KNOWN_HOSTS_OTHER:
+ case SSH_KNOWN_HOSTS_OTHER:
+ keymatch = CURLKHMATCH_MISMATCH;
+ break;
case SSH_KNOWN_HOSTS_NOT_FOUND:
case SSH_KNOWN_HOSTS_UNKNOWN:
case SSH_KNOWN_HOSTS_ERROR:
Cronología: enviado el 20-may-2026 y divulgado el 24-jun-2026.
Qué aprender de este caso
- Cuando una biblioteca traduce los estados de otra (aquí, los resultados de known_hosts de libssh a
curl_khmatch), revisa elswitchcaso a caso: agrupar varioscaseen una sola rama es donde un estado de "posible ataque" acaba confundido con uno inocuo. - En verificación de claves de host, todo estado que no sea un "no conozco este host" claro debería caer en el lado restrictivo (rechazo o aviso), nunca en el permisivo; en caso de duda, que la rama por defecto sea la del rechazo.
- Si usas un callback TOFU con
CURLOPT_SSH_KEYFUNCTION, ten en cuenta que su seguridad depende de queMISSINGsignifique de verdad "host nunca visto"; comprobar en el fichero si el host ya tiene alguna entrada antes de añadir una nueva cierra este hueco. - Al auditar clientes SSH, prueba el escenario "host registrado con RSA, servidor que presenta Ed25519": es el caso límite que diferencia un cambio de tipo de clave de un host realmente nuevo.