Reports
BAJA[CORRUPCIÓN DE MEMORIA]#3754343

CVE-2026-9546: fuga del Referer anterior en libcurl al reutilizar un easy handle

Poner CURLOPT_REFERER a NULL no limpiaba el estado interno del Referer en libcurl, así que la siguiente petición en el mismo easy handle podía reenviar el Referer anterior a otro servidor y provocar un use-after-free.

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 documenta que la cabecera Referer: se puede volver a desactivar poniendo la opción CURLOPT_REFERER a NULL. El investigador comprobó que, hecho esto después de una transferencia, el estado interno del referer asociado al easy handle no se limpiaba: data->state.referer seguía apuntando a memoria ya liberada.

La consecuencia tiene dos caras. Por un lado, la siguiente petición HTTP hecha sobre ese mismo handle podía volver a enviar el Referer: de la petición anterior —aunque la aplicación lo hubiera borrado con la API documentada— a un origen distinto. Por otro, al tratarse de un puntero colgante, la siguiente petición provocaba de forma fiable un heap-use-after-free dentro de la propia librería (visible bajo AddressSanitizer), y si el bloque liberado se reutilizaba antes de la siguiente transferencia, libcurl podía enviar contenido ajeno del heap como valor del Referer:.

El caso es relevante porque no es un mal uso de la API: curl_easy_perform(3) permite expresamente reutilizar un mismo easy handle para varias transferencias y cambiar opciones entre ellas con curl_easy_setopt(3), y CURLOPT_REFERER(3) dice explícitamente que ponerlo a NULL lo desactiva. La aplicación hacía justo lo documentado.

Datos del caso: programa curl, debilidad Use After Free (CWE-416), CVE-2026-9546, severidad baja. Reproducido sobre la rama master de curl (curl-8_20_0-154-gd24652971a, commit d24652971a636a4b2374367434ed72bb561b79bc) en Linux x86_64. No requiere funciones experimentales, backend TLS concreto ni código de depuración: ocurre en la construcción normal de una petición HTTP.

Pasos de reproducción

La secuencia de llamadas a la API que dispara el fallo sobre un único easy handle reutilizado es:

/* 1. Se fija un Referer explícito y se hace la primera petición */
curl_easy_setopt(easy, CURLOPT_REFERER, initial_referer);
curl_easy_setopt(easy, CURLOPT_URL, first_url);
curl_easy_perform(easy);

/* 2. Se consulta el puntero del referer usado */
curl_easy_getinfo(easy, CURLINFO_REFERER, &used_referer);

/* 3. Se borra el Referer con la API documentada */
curl_easy_setopt(easy, CURLOPT_REFERER, NULL);

/* 4. Se vuelve a consultar: el puntero sigue siendo el mismo (colgante) */
curl_easy_getinfo(easy, CURLINFO_REFERER, &stale_referer);
/* stale_referer == used_referer  ->  "stale pointer persisted" */

/* 5. La siguiente petición en el mismo handle dispara el use-after-free */
curl_easy_setopt(easy, CURLOPT_URL, second_url);
curl_easy_perform(easy);

El PoC del reporte es un script autocontenido que, desde la raíz del repositorio de curl, compila libcurl con AddressSanitizer (-fsanitize=address, build de depuración solo con HTTP), levanta un servidor HTTP local en Python que registra la cabecera Referer: recibida, y ejecuta un cliente C que realiza la secuencia anterior. Tras el CURLOPT_REFERER=NULL, CURLINFO_REFERER devuelve el mismo puntero (se imprime stale pointer persisted after CURLOPT_REFERER=NULL) y la segunda petición aborta con AddressSanitizer: heap-use-after-free. El reporte incluye además una variante que demuestra que, cuando el bloque liberado vuelve a ocuparse, el servidor local recibe en la segunda petición un Referer: con contenido que no pertenecía a esa petición.

Marcadores de éxito esperados:

[+] ASan phase reproduced libcurl heap-use-after-free
AddressSanitizer: heap-use-after-free
stale pointer persisted after CURLOPT_REFERER=NULL
[*] captured /leak-second Referer:
https://reused.invalid/BBBB...
[+] vulnerable:

Según el reporte, el comportamiento tiene su origen en estos puntos del código fuente:

  • El setter de CURLOPT_REFERER solo libera/sustituye set.str[STRING_SET_REFERER]: lib/setopt.c.
  • Curl_pretransfer() enlaza data->state.referer únicamente cuando la cadena del referer no es NULL, y no limpia el bufref colgante en la ruta de borrado documentada: lib/transfer.c.
  • La siguiente petición HTTP formatea data->state.referer dentro de la cabecera de la petición: lib/http.c.
  • CURLINFO_REFERER devuelve también ese mismo puntero colgante: lib/getinfo.c.

Impacto

El escenario de mayor riesgo descrito por el investigador es el de aplicaciones de servidor que mantienen un easy handle por worker para reutilizar conexiones, algo que la propia libcurl fomenta: fetchers, rastreadores, servicios de captura de pantalla, relés de webhooks, intermediarios de SSRF o proxies multiinquilino.

La secuencia problemática entre dos trabajos distintos sería:

  1. El inquilino A hace una petición que necesita un Referer: propio (por ejemplo para pasar una comprobación de origen o una política antihotlinking), y ese referer contiene una URL de panel privado, un token opaco de trabajo o una ruta interna.
  2. Al terminar, el framework llama a curl_easy_setopt(easy, CURLOPT_REFERER, NULL) tal como está documentado, manteniendo vivo el handle.
  3. El inquilino B hereda después ese mismo *worker*/handle y pide traer una URL controlada por él.
  4. El servidor de B recibe el Referer: obsoleto de A, u otra cadena del heap que haya reutilizado el bloque liberado.

Es decir, una fuga de confidencialidad entre trabajos o inquilinos, observable en la red por el siguiente origen (que puede ser el atacante). Además, la misma secuencia válida de la API produce un heap-use-after-free interno que puede hacer caer el proceso bajo cualquier detección normal de seguridad de memoria. El propio investigador propuso clasificarlo como exposición de datos entre peticiones de severidad baja, siendo el use-after-free el mecanismo de implementación.

Qué aprender de este caso

  • Al borrar una opción de libcurl con NULL (CURLOPT_REFERER y, por el mismo patrón, otras cadenas de estado por handle), conviene no asumir que el estado interno derivado queda limpio: aquí el setter tocaba set.str[...] pero data->state.referer seguía apuntando al valor ya liberado.
  • Si auditas una librería que cachea punteros derivados de opciones (aquí el par set.str frente a state), revisa específicamente la ruta de "poner a NULL": es fácil que el código limpie o reenlace el estado solo en la rama donde el nuevo valor es no nulo, dejando un puntero colgante en la rama de borrado.
  • En servicios que reutilizan un easy handle entre trabajos o inquilinos distintos, una defensa robusta es no reutilizar el handle al cambiar de contexto de confianza (usar curl_easy_reset() o un handle nuevo por trabajo), en vez de confiar en borrar opción por opción.
  • CURLINFO_REFERER devolvía el mismo puntero colgante: un getinfo que sigue devolviendo un valor tras haber borrado la opción es una señal útil de que el estado interno no se limpió de verdad.