Reports
MEDIA[AUTENTICACIÓN]#3744543

libcurl filtraba la autenticación Digest de un proxy a otro al cambiar el proxy por variables de entorno

Al reutilizar un mismo handle de libcurl 8.20.0, si el proxy cambiaba vía http_proxy o ALL_PROXY, el segundo proxy recibía una cabecera Proxy-Authorization Digest calculada para el primero, reutilizable contra él.

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

Este reporte es una variante de un fallo anterior de curl, CVE-2026-7168, cuya corrección resultó ser incompleta. El problema original: si una aplicación reutilizaba el mismo easy handle de libcurl y cambiaba de proxy entre una transferencia y otra, el estado de autenticación Digest negociado con el primer proxy se enviaba al segundo.

En libcurl 8.20.0 ese caso ya estaba arreglado cuando el proxy se cambia con CURLOPT_PROXY: el estado de autenticación del proxy anterior se limpia. Pero el investigador comprobó que la limpieza no ocurre cuando el proxy efectivo cambia porque cambian las variables de entorno (http_proxy o ALL_PROXY). En ese camino, el handle conserva el estado Digest del proxy A y lo usa para construir la cabecera Proxy-Authorization que manda al proxy B.

La petición que recibía el proxy B tenía este aspecto:

GET http://example.test/protected HTTP/1.1
Host: example.test
Proxy-Authorization: Digest username="silly", realm="realmA", nonce="nonceA", uri="/protected", ...

Tanto realmA como nonceA los había emitido el proxy A, no el B. El fallo recibió el identificador CVE-2026-8927; HackerOne lo clasifica como autenticación incorrecta y con severidad media. El reporte se envió el 18 de mayo de 2026 y se divulgó el 24 de junio de 2026.

Pasos de reproducción

El investigador lo probó contra la versión oficial de curl/libcurl 8.20.0 (cabeceras y biblioteca en tiempo de ejecución de la misma versión). La prueba es totalmente local:

  1. Compilar o instalar curl/libcurl 8.20.0.
  2. Levantar dos proxies HTTP en 127.0.0.1 en puertos aleatorios:
  • proxy A: responde a la primera petición con un reto Digest y a la segunda (ya autenticada) con 200 OK.
  • proxy B: simplemente guarda la primera petición que recibe y responde 200 OK.

El reto que envía el proxy A es:

HTTP/1.1 407 Proxy Authentication Required
Proxy-Authenticate: Digest realm="realmA", nonce="nonceA", qop="auth", algorithm=MD5
Content-Length: 0
Connection: keep-alive
  1. Compilar un cliente en C que use un único easy handle con CURLOPT_PROXYAUTH en CURLAUTH_DIGEST y sin CURLOPT_PROXY, de modo que el proxy salga de http_proxy:
CURL *h = curl_easy_init();

curl_easy_setopt(h, CURLOPT_URL, "http://example.test/protected");
curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, sink);
curl_easy_setopt(h, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST);
curl_easy_setopt(h, CURLOPT_VERBOSE, 1L);

char envA[256];
char envB[256];

snprintf(envA, sizeof(envA), "http://silly:[email protected]:%s", pa);
snprintf(envB, sizeof(envB), "http://silly:[email protected]:%s", pb);

/* Primera transferencia: proxy tomado de http_proxy (proxy A) */
setenv("http_proxy", envA, 1);
CURLcode r1 = curl_easy_perform(h);

/* Segunda transferencia: mismo handle, http_proxy apunta ahora al proxy B */
setenv("http_proxy", envB, 1);
CURLcode r2 = curl_easy_perform(h);

curl_easy_cleanup(h);
  1. Ejecutar el cliente con un entorno limpio (sin http_proxy, HTTP_PROXY, all_proxy, ALL_PROXY, no_proxy ni NO_PROXY heredados) pasándole los dos puertos.
  2. En la primera transferencia, libcurl recibe el reto del proxy A y se autentica con Digest.
  3. En la segunda, el mismo handle va al proxy B. Revisar la petición capturada por B: contiene Proxy-Authorization: Digest con realm="realmA" y nonce="nonceA".
  4. Para confirmar el impacto, el investigador reenvió al proxy A la cabecera capturada por B, y A la aceptó.

Resultado resumido de la prueba completa:

{
  "runtime": "8.20.0",
  "headers": "8.20.0",
  "B_received_proxy_authorization": true,
  "B_digest_valid_for_proxyA": true,
  "replay_status": "HTTP/1.1 200 OK"
}

Casos de control

Para descartar falsos positivos, el investigador comparó varias situaciones:

| Caso | Resultado | |---|---| | Mismo handle, http_proxy cambia de A a B | Vulnerable: se envía el Digest antiguo | | Mismo handle, ALL_PROXY cambia de A a B | Vulnerable: se envía el Digest antiguo | | CURLOPT_PROXY cambia de A a B | No vulnerable | | Handle nuevo para la segunda petición | No vulnerable | | curl_easy_reset() antes de la segunda petición | No vulnerable | | CURLOPT_PROXY = NULL antes de la segunda petición | No vulnerable | | El proxy B no lleva credenciales | No vulnerable en su prueba | | El proxy B lleva credenciales (o solo usuario) | Vulnerable |

Impacto

Un proxy B malicioso recibe una respuesta Digest calculada a partir del reto del proxy A. En la prueba esa respuesta seguía siendo válida para A y se pudo reutilizar con éxito, de modo que el operador de B podía capturar material de autenticación pensado para A y reutilizarlo ante ese proxy en nombre de la víctima. Es la misma clase de problema que CVE-2026-7168: el estado de autenticación de un proxy cruza la frontera y queda expuesto a otro.

Afecta a aplicaciones que reutilizan el mismo easy handle y en las que el proxy efectivo cambia entre transferencias a través de las variables de entorno, con autenticación Digest hacia el proxy.

Remediación

El reporte consta como resuelto y se le asignó CVE-2026-8927; el original no detalla el parche aplicado. La propuesta del investigador era aplicar la misma limpieza que ya se hacía con CURLOPT_PROXY cuando el proxy resuelto cambia entre transferencias por las variables de entorno, reiniciando al menos este estado (o su equivalente) antes de generar una nueva cabecera Proxy-Authorization:

data->state.proxydigest
data->state.authproxy

En versiones sin corregir, los propios casos de control muestran cómo evitarlo desde la aplicación: usar un handle nuevo, llamar a curl_easy_reset() o poner CURLOPT_PROXY a NULL antes de la siguiente transferencia.

Qué aprender de este caso

  • Cuando se corrige un fallo ligado a un "cambio de destino", hay que localizar todos los caminos por los que ese destino puede cambiar: aquí el arreglo cubría CURLOPT_PROXY, pero no el proxy resuelto desde http_proxy o ALL_PROXY. Revisar el fix de un CVE buscando entradas alternativas es una fuente clásica de variantes.
  • El estado de autenticación (nonce, realm, contadores Digest) debe ir atado a la identidad del servidor que lo emitió y compararse con el destino efectivo justo antes de enviarlo, no con el valor de una opción concreta.
  • En aplicaciones que reutilizan handles o clientes HTTP con proxies variables, un curl_easy_reset() o un handle nuevo al cambiar de proxy elimina toda esta clase de arrastre de estado.
  • Para probar este tipo de fugas bastan dos servidores locales —uno que emite el reto y otro que solo registra lo que le llega—, como en este PoC; es una técnica reutilizable para cualquier cliente con autenticación de proxy.