Reports
BAJA[DENEGACIÓN DE SERVICIO]#3973201

CVE-2026-86412: una cookie enviada por un servidor hace que curl vacíe el cookie jar y borre las de otros dominios

Un servidor HTTP podía fijar una cookie que curl guardaba en el cookie jar pero no sabía volver a leer. En la siguiente ejecución curl fallaba antes de conectar y, al cerrar, sobrescribía el jar vacío, borrando las cookies de todos los dominios.

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 cómo curl guarda y vuelve a cargar las cookies en formato Netscape (el fichero que se indica con --cookie-jar y se relee con --cookie). Hay dos piezas que no encajan entre sí:

  • Lo que se escribe no siempre se puede leer. parse_cookie_header() admite cabeceras Set-Cookie de hasta MAX_COOKIE_LINE (5000) bytes. Después, get_netscape_format() construye la línea del jar con curl_maprintf añadiendo dominio, ruta, indicadores, caducidad y tabuladores, sin comprobar la longitud final. En cambio, cookie_load() lee cada línea con un dynbuf limitado a 5000 bytes, y ese límite incluye el terminador NUL; como Curl_get_line() además conserva o añade el salto de línea, la línea más larga que se puede recargar es de 4998 bytes. Un registro de 4999 bytes hace que Curl_get_line() devuelva CURLE_TOO_LARGE.
  • Una carga fallida se trata como válida. Aunque la lectura falle, cookie_load() pone ci->running = TRUE sin condiciones (lib/cookie.c:1211). Curl_pretransfer() propaga el error y no llega a abrir conexión, pero en la limpieza curl ve el almacén de cookies como "en marcha", llama a cookie_output() y reemplaza de forma atómica el jar con lo poco (nada) que había cargado. Así se salta la protección contra almacenes incompletos que se añadió por el issue #18621, porque el indicador ya está activado.

El resultado es que un servidor remoto, sin autenticación, puede envenenar el jar con una sola cookie: la siguiente ejecución de curl que use ese fichero falla con CURLE_TOO_LARGE (código 100) y, si el mismo fichero se usa como entrada y salida, queda reducido a la cabecera estándar de 131 bytes.

Versiones afectadas: de curl 8.18.0 a 8.21.0, además de versiones de desarrollo y prerelease de 8.22.0.

Ejemplo de cookie que da exactamente el tamaño problemático: nombre de 4094 bytes, valor v y Path=/ seguido de 870 caracteres a, fijada por un host de 16 bytes (attacker.example). Serializada en el jar ocupa 4999 bytes, uno más de lo que el cargador acepta.

Pasos de reproducción

  1. Compilar una versión afectada de curl (desde 8.18.0) con soporte de cookies HTTP.
  2. Levantar dos hosts virtuales o servidores de prueba: uno que hace de dominio del atacante y otro de un dominio víctima sin relación.
  3. Hacer que el host víctima fije una cookie persistente normal y guardarla en un cookie jar en formato Netscape.
  4. Conectar con el host del atacante usando el mismo fichero como entrada de cookies (--cookie) y como salida (--cookie-jar).
  5. Hacer que la respuesta del atacante fije una cookie que el parser de curl acepte, pero cuyo registro Netscape serializado sea un byte más largo de lo que el cargador puede leer.
  6. Ajustar ese límite con un nombre o valor largo (pero aceptado) y un atributo Path largo, teniendo en cuenta el dominio, los indicadores, la caducidad, los separadores, el salto de línea y el NUL que se añaden al serializar y recargar.
  7. Comprobar que curl acepta la cookie, termina la petición y escribe en el jar un registro de 4999 bytes junto a la cookie de la víctima.
  8. Guardar una copia de ese jar envenenado para una prueba de control de solo lectura y anotar cuántas peticiones ha recibido el servidor víctima.
  9. Lanzar una nueva transferencia al dominio víctima con el fichero envenenado como entrada y salida de cookies a la vez.
  10. Ver que curl falla con CURLE_TOO_LARGE antes de conectar, porque el registro de 4999 bytes más el salto de línea y el NUL superan el límite de 5000 bytes del dynbuf.
  11. Confirmar que el servidor víctima no ha recibido ninguna petición nueva.
  12. Revisar el cookie jar después de la limpieza de curl.
  13. Ver el efecto destructivo: el jar original ha sido sustituido por solo la cabecera estándar, sin la cookie del atacante ni ninguna de las cookies de la víctima.
  14. Repetir la carga con la copia guardada, esta vez solo como entrada, sin configurar salida de cookie jar.
  15. Confirmar que la carga sigue fallando con CURLE_TOO_LARGE pero el fichero queda intacto: el borrado lo provoca la escritura de la limpieza.
  16. Preparar un control en el límite en el que la cookie del atacante ocupe 4998 bytes en vez de 4999.
  17. Repetir la carga y el guardado con el mismo fichero y comprobar que el registro más corto se recarga bien, la petición a la víctima se hace y el jar sigue lleno.
  18. Para validar un arreglo: asegurarse de que una carga fallida no marca el almacén como apto para guardarse y de que los límites de cookies aceptadas en memoria coinciden con lo que el cargador puede releer.
  19. Repetir la prueba de 4999 bytes y verificar que curl puede rechazar la transferencia, pero nunca sobrescribe el jar original desde un almacén cargado a medias.

Comportamiento esperado: una cookie fijada por un servidor y escrita por curl debe poder recargarse o rechazarse antes de guardarse, y una carga fallida nunca debe acabar sobrescribiendo el jar con un estado incompleto.

Comportamiento vulnerable: curl escribe un registro que su propio cargador rechaza. El error de carga se produce antes de la siguiente conexión, pero la limpieza da por bueno el almacén incompleto y reemplaza el jar compartido, borrando las cookies de todos los dominios que contenía.

Impacto

Un servidor HTTP remoto y sin autenticación podía:

  1. Bloquear todas las transferencias posteriores de curl que cargaran el jar envenenado, que fallan con CURLE_TOO_LARGE antes de hacer ninguna conexión.
  2. En el uso documentado de un mismo fichero para --cookie y --cookie-jar, borrar de forma permanente las cookies de otros dominios que no tienen nada que ver con el atacante.

En la práctica, eso puede destruir sesiones y estado persistente guardado en el jar, cerrar sesiones, romper flujos automatizados o cambiar el comportamiento de peticiones posteriores. No hay fuga de cookies ni corrupción de memoria. El investigador señala que guardar cookies es opcional y que la condición es muy estrecha (un solo byte), por lo que esperaba una severidad baja. Lo relaciona con el issue #18621 y con el caso de rutas con tabulador no recargables arreglado en el PR #21185, aunque con un efecto más amplio.

Remediación

El reporte figura como resuelto y se le asignó el CVE-2026-86412. El investigador propuso dos cambios:

  1. No poner ci->running = TRUE cuando cookie_load() falla, para que la limpieza no pueda sobrescribir el jar a partir de una carga incompleta.
  2. Hacer coherente el límite de cookies aceptadas en memoria con el tamaño máximo de registro Netscape que el cargador puede leer, ya sea comprobando la longitud serializada en get_netscape_format() o ampliando el límite del dynbuf para tener en cuenta los campos añadidos.

Qué aprender de este caso

  • Cuando un programa escribe un formato y luego lo relee, compara los límites de las dos funciones: aquí el parser admitía 5000 bytes de cabecera, pero el registro final crecía con dominio, ruta y tabuladores y el lector solo aceptaba 4998. Probar justo en el byte frontera (4998 frente a 4999) es lo que destapa el fallo.
  • Revisa los indicadores de estado que se activan aunque la operación anterior falle, como ci->running = TRUE tras un cookie_load() fallido: si otro código decide guardar o borrar en función de ese indicador, un error se convierte en pérdida de datos.
  • Un fichero compartido por varios dominios (jar de cookies, caché, almacén de credenciales) que se reescribe entero al cerrar es un punto delicado: si la carga no se completó, no hay que persistir nada en lugar de escribir el estado parcial.
  • Al buscar fallos parecidos en clientes HTTP, mira qué datos controlados por el servidor (longitud de Path, nombre o valor de la cookie) acaban en ficheros locales y si una sola respuesta puede dejar ese fichero ilegible para las siguientes ejecuciones.