Reports
BAJA[CORRUPCIÓN DE MEMORIA]#4123190

curl: un redirect fallido deja un lector obsoleto y la siguiente subida lee memoria liberada (use-after-free)

Si un redirect tras un POST falla, libcurl conserva el lector del cuerpo anterior. La siguiente subida por FTP, SFTP, file o TFTP en el mismo handle lee bloques de 64 KB de memoria ya liberada y los envía al destino.

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 vive en la forma en que libcurl reinicia el estado de una petición cuando se reutiliza el mismo easy handle para varias transferencias, que es precisamente el uso que recomienda la documentación.

Cuando un servidor responde con un redirect a una petición con cuerpo (por ejemplo, un POST), http_perhapsrewind() deja activada la marca req->rewind_read. Si el redirect falla antes de que Curl_http_follow() llegue a su reinicio "suave", esa marca se queda puesta. Dos formas de provocarlo, ambas con CURLOPT_FOLLOWLOCATION:

  • Un 307 con una cabecera Location: http://[::1 mal formada (CURLE_URL_MALFORMAT).
  • Un 307 con una línea de cabecera sin dos puntos (CURLE_WEIRD_SERVER_REPLY).

Al empezar la siguiente transferencia, Curl_connect() ejecuta Curl_req_hard_reset() (lib/request.c, línea 124 en curl-8_23_0). Esta función llama a Curl_client_reset() mientras rewind_read sigue activo, y en ese caso la pila de lectores del cliente se conserva para el rebobinado (lib/sendf.c, línea 82). La marca no se limpia hasta la línea 157, así que esa pila antigua ni se rebobina ni se libera: queda instalada para la transferencia nueva.

Los protocolos de subida que no instalan su propio lector (ftp, ftps, sftp, scp, file y tftp) leen a través de Curl_client_read() con esa pila heredada (file.c línea 369, ftp.c línea 561). Si entre medias la aplicación ha cambiado los postfields, libcurl libera su copia interna creada con CURLOPT_COPYPOSTFIELDS (setopt.c línea 2026), y cr_buf_read() acaba haciendo memcpy() de 65536 bytes por lectura desde memoria liberada (sendf.c línea 1334), que se envían al destino de la subida.

  • Versiones afectadas: de la 8.16.0 (el orden de operaciones llegó con el commit a93113b5b4) a la 8.23.0; según el investigador, la rama principal seguía igual al reportarlo.
  • Debilidad: Use After Free (CWE-416).
  • CVE: CVE-2026-111954.
  • Severidad asignada: baja.
  • Fechas: enviado el 17-oct-2026 y divulgado el 26-oct-2026.

El investigador indica que usó herramientas de IA para ayudarse con los programas de prueba, pero que la reproducción y las cifras son suyas.

Pasos de reproducción

Entorno del investigador: curl 8.23.0 compilado desde la etiqueta curl-8_23_0 (3163ec0656), Linux x86_64, OpenSSL 3.6.4, una compilación con clang y -fsanitize=address y otra con gcc y --enable-debug.

  1. Levantar el servidor HTTP de prueba adjunto al reporte. Lee la cabecera de la petición y un byte del cuerpo, responde con 307, Location: http://[::1 y Content-Length: 0, y cierra la conexión:

`` python3 http_307bad.py 8900 ``

Para la segunda variante (cabecera sin dos puntos) se lanza con --badheader --location /ok.

  1. Compilar reader_reuse.c (adjunto) contra la libcurl con ASAN. El programa usa un único easy handle:
  • Primera transferencia: POST con CURLOPT_COPYPOSTFIELDS (de BODY bytes) y CURLOPT_FOLLOWLOCATION.
  • Después, sin curl_easy_reset(): pone CURLOPT_POSTFIELDS a NULL, activa CURLOPT_UPLOAD, asigna un FILE* a CURLOPT_READDATA y cambia a una segunda URL.
  1. Subida a fichero local:

`` BODY=1000000 ./reader_reuse http://127.0.0.1:8900/post file:///tmp/out ``

La primera transferencia devuelve 3 y ASAN informa de un heap-use-after-free de lectura de 65536 bytes en cr_buf_read (sendf.c:1334), llamado desde file_do (file.c:369). La memoria se liberó en setopt.c:2026 y se reservó en setopt.c:1811.

  1. Lo mismo, pero con ftp://127.0.0.1:PORT/x como segunda URL contra cualquier servidor FTP que acepte STOR: mismo informe de ASAN, ahora desde Curl_req_send_more (request.c:468). En la compilación de depuración sin ASAN el proceso muere con SIGSEGV dentro del memcpy de cr_buf_read, porque glibc ya ha desmapeado ese bloque de 1 MB.
  1. Con BODY=100000 (por debajo del umbral de mmap) no hay caída: la segunda transferencia devuelve 0 como si todo fuese bien, pero el destino recibe bytes del búfer liberado en lugar del contenido de READDATA.
  1. Control: con la opción --reset (que llama a curl_easy_reset() entre las dos transferencias), la segunda subida usa el fichero de READDATA como se espera.
  1. Con el parche fix-pretransfer-drop-stale-readers.patch aplicado y recompilando, se repiten los pasos 3 y 4: terminan limpios y se sube el contenido real de READDATA.

Impacto

  • Divulgación de memoria: la subida envía bloques de 64 KB de memoria ya liberada al destino, que puede ser un servidor FTP, SFTP o TFTP remoto o un fichero local.
  • Corrupción silenciosa: con un cuerpo de 100 KB la transferencia se da por correcta y el destino recibe bytes del búfer liberado en lugar de lo que la aplicación quería subir.
  • Caída de la aplicación: con cuerpos grandes (en la prueba, 1 MB) la lectura provoca un SIGSEGV.

Condiciones: un servidor tiene que responder a una petición con cuerpo con un redirect que curl no pueda seguir, y la aplicación tiene que reutilizar el handle para una subida por uno de los protocolos citados, haber cambiado los postfields y no llamar a curl_easy_reset() entre medias. La herramienta de línea de órdenes curl crea un handle por transferencia y no está afectada; sí lo están los bindings de lenguajes que mantienen un handle por objeto.

Remediación

El investigador adjuntó un parche (fix-pretransfer-drop-stale-readers.patch) de seis líneas en Curl_pretransfer() que descarta cualquier rebobinado pendiente al iniciar una transferencia nueva. Con él la reproducción queda limpia y el conjunto de tests de curl no cambia. El reporte figura como resuelto y con CVE asignado, aunque no detalla qué cambio exacto se incorporó finalmente.

Mientras tanto, llamar a curl_easy_reset() entre transferencias en el mismo handle evita el problema.

Qué aprender de este caso

  • Las marcas de estado que sobreviven a un camino de error (aquí rewind_read, puesta antes de un redirect que falla) son un buen sitio donde buscar: revisa qué se limpia en el camino "feliz" (el reinicio suave de Curl_http_follow()) y qué se queda colgando si se aborta antes.
  • En funciones de reinicio, el orden importa: Curl_req_hard_reset() consulta la marca antes de limpiarla. Si un reset decide conservar recursos según un flag, ese flag debería resetearse primero o comprobarse al empezar la siguiente operación, como hace el parche en Curl_pretransfer().
  • Cuando una librería guarda un puntero a un búfer que una opción posterior puede liberar (CURLOPT_COPYPOSTFIELDS frente a CURLOPT_POSTFIELDS), cualquier objeto de larga vida que siga apuntando a él es candidato a use-after-free: busca quién más conserva ese puntero.
  • Para probar estos fallos, combina reutilización de handles, respuestas de servidor deliberadamente rotas y distintos tamaños de cuerpo: aquí solo los tamaños grandes provocaban una caída visible, mientras que los pequeños fallaban en silencio.