Reports
MEDIA[TLS Y CRIPTOGRAFÍA]#4128428

CVE-2026-115528: curl con wolfSSL acepta servidores TLS sin certificado y les envía las credenciales

Compilado con wolfSSL, curl completaba el handshake TLS 1.2 con un servidor que usaba un cifrado anónimo (sin certificado) al conectar por nombre de host, pese a tener activada la verificación, y le enviaba credenciales y datos.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

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

  1. Levantar un servidor TLS 1.2 sin certificado en 127.0.0.1:34500 que registre las cabeceras recibidas. El investigador lo hizo con el módulo ssl de Python: configurando ctx.set_ciphers('ADH-AES128-SHA:@SECLEVEL=0'), cargando parámetros DH con ctx.load_dh_params(...) y sin llamar a load_cert_chain.
  1. 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/
  1. 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.