Reports
BAJA[AUTENTICACIÓN]#3969300

curl con OpenSSL ignoraba --pinnedpubkey con --insecure si el servidor no enviaba certificado (CVE-2026-80230)

Con --insecure y un cifrado anónimo, curl compilado con OpenSSL se saltaba la comprobación del pin de clave pública y completaba la conexión, permitiendo un ataque MITM pese al pin.

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

El fallo está en Curl_ossl_check_peer_cert(), la función de libcurl que, en compilaciones con OpenSSL, revisa el certificado del servidor una vez hecho el handshake TLS. Tiene asignado el CVE-2026-80230.

Quien usa curl con --insecure y a la vez --pinnedpubkey está diciendo: "no valides la cadena de certificados ni el nombre del host, pero exige que la clave pública del servidor sea esta". El pin pasa a ser la única garantía de que se habla con el servidor correcto. El problema es que esa garantía desaparecía sin aviso cuando el servidor no presentaba ningún certificado.

Eso ocurre con los cifrados anónimos, como AECDH-AES128-SHA. La secuencia dentro de la función era esta:

  1. Se llama a SSL_get1_peer_certificate() (línea 4795). Sin certificado, devuelve NULL.
  2. Una comprobación en las líneas 4798–4799 evalúa !(conn_config->verifypeer || conn_config->verifyhost). Con --insecure ambas opciones están desactivadas, así que la condición es verdadera.
  3. Se ejecuta goto out (línea 4799), que salta directamente a la línea 4876 y deja atrás la llamada a ossl_check_pinned_key() de la línea 4874.
  4. La variable result sigue valiendo CURLE_OK, de modo que la función informa de éxito y curl completa la transferencia sin haber mirado el pin.

Lo esperable era que curl abortara con CURLE_SSL_PINNEDPUBKEYNOTMATCH (código de salida 90).

HackerOne muestra la severidad del reporte como "none"; aquí se clasifica como baja por las muchas condiciones que exige.

Pasos de reproducción

Tienen que darse todas estas condiciones:

  • libcurl compilado con OpenSSL.
  • El cliente desactiva la verificación: --insecure (o CURLOPT_SSL_VERIFYPEER=0 y CURLOPT_SSL_VERIFYHOST=0).
  • El cliente fija un pin con --pinnedpubkey.
  • El cliente admite un cifrado anónimo, por ejemplo --ciphers AECDH-AES128-SHA:@SECLEVEL=0.
  • Se limita la conexión a TLS 1.2 o inferior (--tls-max 1.2), porque TLS 1.3 no admite autenticación anónima.
  • El servidor usa ese mismo cifrado anónimo y no tiene certificado cargado.

Pasos:

  1. Levantar un servidor TLS (el investigador usó un script en Python, poc.py) que solo ofrezca AECDH-AES128-SHA y sin certificado.
  2. Lanzar curl contra él con un pin que no puede coincidir con nada:
/opt/curl/bin/curl --insecure \
  --pinnedpubkey "sha256//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" \
  --ciphers "AECDH-AES128-SHA:@SECLEVEL=0" \
  --tls-max 1.2 \
  https://127.0.0.1:PORT/
  1. Comprobar el resultado: curl termina con código 0 y muestra la respuesta HTTP. Tendría que haber fallado con el código 90 (CURLE_SSL_PINNEDPUBKEYNOTMATCH).
  2. Como control, repetir la misma orden con el mismo pin erróneo contra un servidor con un certificado real: curl sale con 90. Eso demuestra que el pin solo se ignora cuando no hay certificado.

Impacto

Un atacante en posición de intermediario (MITM) con control de la red puede forzar que se negocie un cifrado anónimo, siempre que el cliente lo permita con --ciphers, e interceptar el tráfico aunque el cliente haya fijado un pin de clave pública.

El alcance es limitado: hace falta que el cliente combine --insecure (o CURLOPT_SSL_VERIFYPEER=0) con --pinnedpubkey y que además admita un cifrado anónimo. Pero en esa configuración el pin es la única autenticación del servidor, y este fallo la anula por completo.

Remediación

El investigador propuso dos soluciones:

  • Antes del goto out de la línea 4799, comprobar si hay un pinned_key configurado. Si lo hay y no existe certificado con el que compararlo, devolver CURLE_SSL_PINNEDPUBKEYNOTMATCH en lugar de dar la conexión por buena.
  • O bien reorganizar la función para que siempre llegue a ossl_check_pinned_key() cuando pinned_key no sea NULL, sin importar cómo estén verifypeer y verifyhost.

El reporte consta como resuelto, pero no detalla cuál de las dos soluciones se aplicó ni en qué versión de curl.