Resumen
Con las opciones por defecto, curl verifica el certificado del servidor (CURLOPT_SSL_VERIFYPEER=1) y que corresponda al nombre de host (CURLOPT_SSL_VERIFYHOST=2). La documentación de ambas opciones es clara: si la verificación no puede probar que el certificado está firmado por una CA, o que se emitió para ese host, la conexión falla.
En las compilaciones de curl que usan wolfSSL como biblioteca TLS esa promesa no se cumplía en un caso concreto: cuando el servidor negocia un cifrado anónimo de Diffie-Hellman (por ejemplo ADH-AES128-SHA en TLS 1.2) y, por tanto, no envía ningún certificado. curl terminaba el handshake, devolvía ssl_verify_result=0 como si todo estuviera bien y mandaba la petición completa, credenciales incluidas.
El fallo está en lib/vtls/wolfssl.c, en la función wssl_connect() (líneas 1717 a 1734 en curl 8.23.0). Allí hay una comprobación explícita de que el servidor ha presentado certificado, pero solo se ejecuta cuando !connssl->peer.sni, es decir, cuando se conecta a una dirección IP literal. Para los nombres de host, curl delega en WOLFSSL_VERIFY_PEER y en wolfSSL_check_domain_name(), y ninguno de los dos actúa si no llega certificado alguno: sin certificado no hay nada que rechazar, y el cifrado anónimo pasa.
La prueba de que la comprobación debería existir está en el propio curl: contra el mismo servidor, pero usando https://127.0.0.1/, la conexión se rechaza con "unable to get peer certificate".
El problema afecta a instalaciones reales: los paquetes habituales de wolfSSL traen los cifrados anónimos activados. Los de Debian (libwolfssl en bookworm, trixie y sid) y la versión de Homebrew definen HAVE_ANON y OPENSSL_COMPATIBLE_DEFAULTS en options.h.
Versiones probadas: curl 8.23.0 y la rama master (commit 7d091c71fd) compiladas con wolfSSL 5.9.4 (Homebrew) en macOS, tanto la herramienta curl como libcurl. Las compilaciones con OpenSSL o GnuTLS no están afectadas.
Pasos de reproducción
- Levantar un servidor TLS 1.2 sin certificado en
127.0.0.1:34500que registre las cabeceras recibidas. El investigador lo hizo con el módulosslde Python: configurandoctx.set_ciphers('ADH-AES128-SHA:@SECLEVEL=0'), cargando parámetros DH conctx.load_dh_params(...)y sin llamar aload_cert_chain.
- Lanzar contra él un curl compilado con wolfSSL, usando el nombre
localhost(no la IP) y unas credenciales de prueba:
curl --cacert ca.pem -u alice:S3cret -w 'http=%{http_code} verify=%{ssl_verify_result}\n' https://localhost:34500/
- Observar el resultado: curl da la verificación por buena y el servidor recibe la cabecera de autenticación.
http=200 verify=0
-v: SSL connection using TLSv1.2 / ADH-AES128-SHA
server log:
handshake ok cipher=ADH-AES128-SHA (server sent no certificate)
GET / HTTP/1.1
Authorization: Basic [REDACTADO]
Comprobaciones de control contra el mismo servidor:
- Compilación con OpenSSL:
curl: (35) ... handshake failure, sin enviar nada. - Compilación con GnuTLS:
curl: (35) GnuTLS, handshake failed. - wolfSSL usando
https://127.0.0.1:34500/:curl: (60) unable to get peer certificate. - Con wolfSSL, un servidor con certificado firmado por la CA sigue devolviendo 200 y uno con una CA no confiable se rechaza (código 35). Es decir, la verificación funciona; lo que faltaba era el caso de "no hay certificado".
Impacto
Un atacante situado en la ruta de red puede responder al handshake de un curl -u user:pass https://api.example.com/ corriente, o de cualquier aplicación que use libcurl con las opciones por defecto, con un ServerHello de DH anónimo y sin certificado. curl compilado con wolfSSL acepta la conexión y le entrega las credenciales y el cuerpo de la petición. Es una evasión completa de la verificación de certificados sin que el usuario haya cometido ningún error.
Remediación
El reporte figura como resuelto y se le asignó el CVE-2026-115528. El investigador propuso, y verificó, un parche que añade en wssl_connect() la comprobación de certificado para todas las conexiones, no solo para las de IP literal, siempre que esté activada verifypeer o verifyhost:
--- a/lib/vtls/wolfssl.c
+++ b/lib/vtls/wolfssl.c
@@ -1733,6 +1733,16 @@ static CURLcode wssl_connect(struct Curl_cfilter *cf,
wolfSSL_X509_free(cert);
}
+ if(ret == WOLFSSL_SUCCESS &&
+ (conn_config->verifypeer || conn_config->verifyhost)) {
+ WOLFSSL_X509 *cert = wolfSSL_get_peer_certificate(wssl->ssl);
+ if(!cert) {
+ failf(data, "SSL: could not get peer certificate");
+ return CURLE_PEER_FAILED_VERIFICATION;
+ }
+ wolfSSL_X509_free(cert);
+ }
+
if(ret == WOLFSSL_SUCCESS) {
return CURLE_OK;
}
Según las pruebas del investigador, con ese cambio el servidor anónimo se rechaza con el código 60 y no recibe ninguna petición, mientras que el servidor con certificado firmado por la CA sigue devolviendo 200.
Qué aprender de este caso
- En código que soporta varias bibliotecas TLS, compara las ramas de verificación de cada backend y de cada tipo de destino: aquí la comprobación de "hay certificado" existía, pero solo dentro del camino de IP literal (
!connssl->peer.sni), y el camino por nombre de host se quedaba sin ella. - Activar "verificar el par" en una biblioteca TLS no garantiza que se rechace la ausencia de certificado: si se negocia un cifrado anónimo, la validación de cadena y de nombre simplemente no se ejecutan. Después del handshake, exige de forma explícita que
get_peer_certificate()devuelva algo. - Al probar un cliente TLS, incluye un servidor que no presente certificado (suites
ADH-*) además de los clásicos de certificado caducado, autofirmado o con nombre equivocado, y pruébalo tanto por nombre como por IP. - Si distribuyes software enlazado con wolfSSL, revisa si el paquete define
HAVE_ANON; excluir los cifrados anónimos de la lista del cliente añade una segunda barrera aunque falle otra comprobación.