Reports
BAJA[AUTENTICACIÓN]#3973190

curl reutilizaba una conexión FTP autenticada con certificado de cliente en transferencias sin TLS ni certificado

Una regresión en la reutilización de conexiones FTP de libcurl permitía que una transferencia sin TLS ni certificado usase una conexión ya autenticada con el certificado de cliente privilegiado de otro handle.

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

libcurl guarda las conexiones en un pool para reutilizarlas entre transferencias. En FTP, una conexión de control puede pasar a TLS con AUTH TLS (STARTTLS) y, en ese momento, presentar un certificado de cliente. El fallo, registrado como CVE-2026-86411, es que una transferencia posterior configurada con CURLUSESSL_NONE y sin certificado podía recibir esa misma conexión del pool. Al reutilizarla, curl no hacía ni un nuevo handshake TLS ni un nuevo login FTP, de modo que el servidor seguía atribuyendo las órdenes a la identidad del certificado del primer handle.

Según el investigador, es una regresión introducida por el commit 4ff212f8ed328fb95261e11add8f4d2f0616895a ("url: connection reuse fixes for starttls") y está muy relacionada con CVE-2016-5420, un fallo antiguo de reutilización de identidad de certificado de cliente entre conexiones. Afecta a curl 8.21.0 y a versiones de desarrollo previas a 8.22.0.

Por qué ocurría

Tres piezas del código de emparejamiento de conexiones encajaban mal:

  1. FTP declara PROTOPT_SSL_REUSE (lib/protocol.c), así que url_match_ssl_use() (lib/url.c:712-730) acepta una conexión ya subida a TLS para una transferencia posterior en ftp:// plano.
  2. Con CURLUSESSL_NONE, el emparejamiento fija may_tls = false y require_tls = false (lib/url.c:1187-1188). url_match_ssl_config() (lib/url.c:883-895) solo llama a Curl_ssl_conn_config_match() si el protocolo tiene PROTOPT_SSL o si may_tls es verdadero. Como no se cumple ninguna de las dos, la configuración del certificado de cliente nunca se comparaba.
  3. El commit citado cambió Curl_ftp_conns_match() (lib/ftp.c:4472-4492): antes exigía que use_ssl fuera idéntico (!=) y pasó a rechazar solo cuando el valor nuevo era mayor que el de la conexión del pool (>). Eso abrió la puerta a la combinación CURLUSESSL_CONTROL → CURLUSESSL_NONE.

Pasos de reproducción

Preparación:

  1. Compilar curl 8.21.0 (u otra revisión afectada) con soporte de FTP, TLS explícito y certificados de cliente.
  2. Crear una CA de pruebas, un certificado de servidor FTP, un certificado de cliente privilegiado y otro sin privilegios.
  3. Levantar un servidor FTP que acepte AUTH TLS, pida certificado de cliente y registre, por cada conexión de control, un identificador estable y la identidad del certificado presentado.
  4. Configurar un recurso u orden FTP que solo funcione si la conexión de control presenta el certificado privilegiado.
  5. Usar el mismo usuario y contraseña FTP en todas las pruebas, para que la única diferencia de autorización venga del certificado TLS.

Prueba principal:

  1. Crear un multi handle de libcurl, que aporta el pool de conexiones compartido.
  2. Crear un primer easy handle hacia el recurso protegido, con CURLOPT_USE_SSL = CURLUSESSL_CONTROL, el certificado y la clave privilegiados, y añadirlo al multi handle.
  3. Ejecutar la transferencia y comprobar que funciona, que el servidor registra la identidad privilegiada y que la conexión de control queda disponible para reutilizar.
  4. Quitar ese easy handle, conservando el multi handle y su pool.
  5. Crear un segundo easy handle al mismo destino FTP, con el mismo usuario y contraseña, pero con CURLOPT_USE_SSL = CURLUSESSL_NONE y sin certificado.
  6. Ejecutar la segunda transferencia y anotar CURLINFO_NUM_CONNECTS, la respuesta FTP, el identificador de conexión del servidor y la identidad de certificado asociada.
  7. Resultado vulnerable: la segunda transferencia reutiliza la conexión de control ya subida a TLS, no abre ninguna nueva y accede al recurso protegido con la identidad privilegiada.

Controles:

  1. Repetir la segunda transferencia con CURLOPT_FRESH_CONNECT activado.
  2. Comprobar que se abre una conexión nueva sin certificado y que el servidor deniega el acceso.
  3. Repetir con TLS explícito y el certificado sin privilegios en el segundo handle.
  4. Comprobar que curl crea una conexión con la identidad sin privilegios y que el servidor deniega el acceso.
  5. Control positivo: usar el mismo certificado privilegiado en ambos handles y confirmar que la reutilización y la autorización son las esperadas.
  6. Para validar un arreglo, exigir que la configuración de identidad de cliente sea compatible siempre que la conexión del pool ya use TLS, aunque la transferencia nueva pida CURLUSESSL_NONE.
  7. Repetir la secuencia original (privilegiado y luego sin certificado) y verificar que el segundo handle ya no hereda la conexión autenticada del primero.

Lo esperado es que una transferencia sin certificado nunca reutilice una conexión de control autenticada con el certificado de otro handle: debe abrir la suya y recibir solo su propia autorización.

Impacto

Un handle sin certificado de cliente heredaba en silencio la sesión TLS y la identidad de certificado de otro handle que había usado el mismo pool. Si el servidor FTP concede permisos según ese certificado, la segunda transferencia podía leer, subir, modificar o borrar recursos a los que no tendría acceso con su propia configuración: un salto de autorización entre handles, no solo un cifrado más fuerte del pedido.

Hacen falta varias condiciones a la vez, por eso el investigador lo valoró como de severidad baja:

  • Los dos handles comparten pool y apuntan al mismo servidor FTP con el mismo USER/PASS.
  • El primero usa CURLUSESSL_CONTROL con un certificado de cliente privilegiado.
  • El segundo usa CURLUSESSL_NONE sin certificado.
  • El servidor FTP decide la autorización a partir del certificado de cliente.

Remediación

El reporte figura como resuelto y con CVE asignado. La corrección propuesta por el investigador es comparar la configuración del certificado de cliente cuando la conexión del pool ya tiene TLS (por STARTTLS), aunque el handle nuevo no pida TLS (may_tls = false). Como mínimo, url_match_ssl_config() debería tener en cuenta si la conexión existente ya está cifrada, y no solo si el handle nuevo quiere cifrado.

Qué aprender de este caso

  • En cualquier pool de conexiones, la comprobación de compatibilidad debe mirar el estado real de la conexión guardada (TLS activo, certificado presentado, usuario autenticado), no solo lo que pide la petición nueva.
  • Cambiar una comparación de igualdad (!=) por una de orden (>) en código de emparejamiento abre combinaciones antes imposibles; ante cambios así en protocolos con STARTTLS conviene probar explícitamente la dirección "de más seguro a menos seguro".
  • Si un servidor autoriza por certificado de cliente sobre una conexión de larga duración, cualquier cliente que reutilice conexiones hereda esa identidad: es un sitio donde buscar fallos parecidos en otras librerías con pooling para FTP, SMTP u otros protocolos con STARTTLS.
  • Cuando un fallo se parece tanto a uno antiguo (aquí CVE-2016-5420), merece la pena convertir la secuencia "con certificado y luego sin él" en un test permanente para que una refactorización no la vuelva a abrir.