SSRF en vLLM: cuando el parche de la allowlist repitió el mismo fallo de parseo
Dos avisos de vLLM muestran el mismo fallo dos veces: una allowlist de hosts validada con un parser de URL y la petición enviada con otro. El parche cambió de parsers y reabrió el bypass.
¿Qué es esto?
Los modelos multimodales de vLLM pueden descargar imágenes, audio y vídeo desde URL incluidas en una petición. Para evitar que esa función se convierta en una primitiva de Server-Side Request Forgery (SSRF), vLLM comprueba el host de cada URL contra una allowlist antes de descargarla. Dos avisos demuestran que ese control se ha eludido dos veces de la misma manera.
El primero, CVE-2026-24779 (GHSA-qh4c-xf7m-gxfc, publicado el 27 de enero de 2026, CVSS 7.1 «alto»), es una SSRF en la clase MediaConnector: el host se valida con un parser de URL y se descarga con otro distinto. El segundo, CVE-2026-25960 (GHSA-v359-jj2v-j536, publicado el 9 de marzo de 2026, CVSS 5.4 «moderado»), es el bypass del parche: la corrección movió la validación a un nuevo parser, pero la ruta de descarga asíncrona seguía usando un tercer parser. La misma clase de fallo, reabierta por la propia mitigación. Ambos son CWE-918, y la cadena queda corregida por completo en vLLM 0.17.0.
Cómo funciona
El patrón es un parser differential: validar la URL con el parser A, enviar la petición con el parser B y confiar en que ambos coincidan en cuál es el host de destino. Cuando discrepan, la allowlist protege un nombre de host que el cliente HTTP nunca llega a contactar.
En el fallo original de MediaConnector, la validación usaba urllib (urlparse) de Python mientras que la descarga se hacía con requests (que se apoya en urllib3). Ambos siguen gramáticas de URL diferentes —una más cercana a la RFC 3986 y otra al estándar WHATWG— y no tratan la barra invertida de igual modo, lo que basta para colar otro host a través de la allowlist.
El parche (PR #32746) unificó la validación en urllib3.util.parse_url(). Pero la ruta de descarga asíncrona, load_from_url_async en vllm/connections.py, envía la petición con aiohttp, que analiza las URL con la biblioteca yarl. El aviso documenta la discrepancia con precisión:
URL de entrada: https://httpbin.org\@evil.com/
urllib3.parse_url() -> host = httpbin.org (codifica "\" como %5C, trata "\@evil.com/" como la ruta)
yarl (vía aiohttp) -> host = evil.com (trata "httpbin.org\" como userinfo, "@" como separador)
Así, el validador ve httpbin.org y aprueba la petición; aiohttp se conecta después a evil.com. La allowlist pasa, pero la petición va a otro sitio. No hace falta ningún payload exótico: la URL de ejemplo canónica está en el aviso público, y la técnica subyacente (confusión entre parsers con la barra invertida y el @/userinfo) es una clase de bypass de filtros SSRF conocida desde hace tiempo, no un ataque nuevo.
Por qué importa
Una SSRF en un servidor de inferencia es más que un curl saliente. vLLM suele desplegarse dentro de clústeres —el aviso menciona topologías tipo llm-d— donde el pod está en una red interna de confianza sin filtrado de salida. Una descarga controlada por la petición permite entonces alcanzar servicios internos visibles desde el pod: endpoints de metadatos, pods vecinos, API de administración y bases de datos. El primer aviso señala que un atacante podría llegar a un endpoint de administración interno y reportar métricas falsas (por ejemplo, un estado erróneo de la caché KV), desestabilizando la flota de servicio.
La lección más amplia es la que repiten ambas CVE. Validar-y-descargar solo es seguro si el mismo componente resuelve el host en los dos momentos. Cambiar urllib→urllib3 por urllib3→yarl corrigió una discrepancia concreta e introdujo otra, porque la arquitectura —«parsear dos veces y dar por hecho que coinciden»— quedó intacta. Los bypass de allowlist rara vez se deben a una sola expresión regular defectuosa; se deben a dos fragmentos de código que discrepan sobre el significado de una cadena.
Defensas
Si ejecuta vLLM con la descarga de URL multimodal, actualice a la versión 0.17.0 o posterior, que corrige la ruta asíncrona (PR #34743).
Más allá del parche, trate la descarga de URL como lo que es: un sumidero de SSRF. Desactive la descarga de medios remotos si su despliegue no la necesita, y proporcione a los modelos bytes pre-descargados. Cuando la descarga sea imprescindible, imponga la frontera en la capa de red en lugar de en el análisis de cadenas: deniegue al pod de inferencia toda salida hacia los rangos de direcciones internas y hacia la IP de metadatos de la nube, de modo que un bypass de allowlist no alcance nada útil.
Para la clase «parser differential» en concreto, no valide una representación de una URL para luego entregar la cadena en bruto a un cliente distinto. Analice una vez, resuelva el host a una dirección IP, compruebe esa IP contra su política y conéctese a esa dirección fijada —de forma que el host aprobado sea el host contactado—. Bloquee las redirecciones en las descargas de medios (vLLM las condiciona mediante VLLM_MEDIA_URL_ALLOW_REDIRECTS), ya que una respuesta 30x es otra manera de mover el destino real después de la comprobación.
Estado
| Elemento | Detalle |
|---|---|
| CVE-2026-24779 (SSRF en MediaConnector) | Publicado el 27 ene. 2026 · CVSS 7.1 alto · corrección PR #32746 |
| CVE-2026-25960 (bypass de la protección SSRF) | Publicado el 9 mar. 2026 · CVSS 5.4 moderado · afecta a vLLM ≥ 0.15.1 · corrección PR #34743 |
| Corregido en | vLLM 0.17.0 |
| Debilidad | CWE-918 (Server-Side Request Forgery) |
Sources
- → https://github.com/advisories/GHSA-qh4c-xf7m-gxfc
- → https://github.com/vllm-project/vllm/security/advisories/GHSA-v359-jj2v-j536
- → https://github.com/vllm-project/vllm/commit/f46d576c54fb8aeec5fc70560e850bed38ef17d7
- → https://github.com/vllm-project/vllm/pull/32746
- → https://github.com/vllm-project/vllm/pull/34743