Omisión de autenticación y RCE en GPUStack, explotada (CVE-2026-102895)
GPUStack anterior a 2.2.0 confía en un origen loopback, así que un proxy inverso local convierte una petición remota en acceso de administrador y ejecución de código.
Resumen
- GPUStack anterior a 2.2.0 trata una petición que parece venir de 127.0.0.1 como el administrador integrado, y un proxy inverso en el mismo host hace que toda petición remota parezca loopback. Un atacante no autenticado obtiene entonces acceso administrativo completo.
- Todo despliegue anterior a 2.2.0 con el valor predeterminado
force_auth_localhost=falsedetrás de un proxy inverso local está expuesto. El aviso también nombra una cabeceraX-GPUStack-Real-IPfalsificada en modo de puerta de enlace integrada. - El proveedor informa de explotación en el mundo real contra un despliegue 2.1.1, con backends de inferencia creados por el atacante que ejecutaban shells inversas. El registro CVE se publicó el 4 de noviembre de 2026. No está en CISA KEV a fecha del 5 de noviembre de 2026.
- Actualice a GPUStack 2.2.0 o posterior y vuelva a crear el contenedor. En versiones anteriores, active
force_auth_localhost=truey elimine la cabeceraX-GPUStack-Real-IPque envíe el cliente en el proxy. Consulte "¿Cómo lo corrijo?".
¿Estoy afectado?
Está expuesto cuando se cumplen todas estas condiciones:
- Su servidor GPUStack ejecuta una versión anterior a 2.2.0.
force_auth_localhostes falso o no está definido, que es el valor predeterminado en esas versiones.- La API es accesible mediante un proxy inverso en el mismo host configurado con
proxy_pass http://127.0.0.1:<port>, así que cada petición remota llega a GPUStack con el par TCP 127.0.0.1. En modo de puerta de enlace integrada, un atacante que pueda establecerX-GPUStack-Real-IP: 127.0.0.1alcanza la misma rama.
La condición del proxy inverso es la que el aviso llama el despliegue autoalojado canónico: un proxy inverso en el mismo host con proxy_pass http://127.0.0.1:<port>. Esa es exactamente la disposición en la que confiaba la rama vulnerable.
| Rama | Versiones afectadas | Versión corregida |
|---|---|---|
| 2.1.x y anteriores | 2.1.1 y todas las versiones anteriores | 2.2.0, publicada el 2026-06-18 |
| 2.2.x | Ninguna (corregido en 2.2.0) | Siga actualizando a la última versión |
Compruebe qué ejecuta:
# Version inside the server container
docker exec gpustack gpustack version
# Python package version
docker exec gpustack pip show gpustack | head -2
# Is the loopback bypass forced off?
docker exec gpustack env | grep -i FORCE_AUTH_LOCALHOST
El último comando no muestra nada cuando el ajuste está en su valor predeterminado. El ajuste también aparece en el archivo de configuración del servidor como force_auth_localhost.
GPUStack es un gestor de clústeres de GPU de código abierto para servir modelos de IA y aprovisionar instancias de GPU. Su imagen Docker tenía 942,824 descargas en Docker Hub el 5 de noviembre de 2026, y el repositorio tenía 5,978 estrellas.
¿Cómo lo corrijo?
- Actualice el servidor a GPUStack 2.2.0 o posterior:
pip install --upgrade "gpustack==2.2.0"
- Vuelva a crear el contenedor con la nueva imagen. Descargar la imagen sin recrear el contenedor deja el proceso antiguo en ejecución.
- Confirme que el atajo loopback ha desaparecido. La opción
force_auth_localhostno hace nada en 2.2.0, y cada petición debe autenticarse. - Revise el estado administrativo por cambios que usted no hizo. El aviso nombra cambios de usuarios y modelos, claves de API de administrador persistentes, secretos de workers y proveedores de nube, y backends de inferencia creados mediante el endpoint reservado a administradores.
Para los despliegues que no pueden actualizarse todavía, el aviso da tres soluciones temporales. Reducen la exposición pero no eliminan el código vulnerable:
- Active
force_auth_localhost=truepara que las peticiones loopback también exijan autenticación. - Elimine cualquier cabecera
X-GPUStack-Real-IPenviada por el cliente en el proxy inverso o la puerta de enlace, y asegúrese de que el proxy no pueda hacer que el tráfico externo parezca loopback. - Mantenga la API fuera de redes no confiables, detrás de una VPN o de autenticación en el borde.
La primera solución es la eficaz para la forma habitual del proxy inverso. Las otras dos limitan la segunda vía y la ventana de exposición.
¿Cuál es la causa raíz?
La causa raíz es una decisión de autenticación basada en el origen de red de la petición. Un par local o una cabecera enviada por el cliente no es una identidad, y un proxy inverso en el mismo host hace que la dirección del par no signifique nada.
Este es el código vulnerable en v2.1.1, en gpustack/api/auth.py. La función de las líneas 51 a 56 solo devuelve la cabecera X-GPUStack-Real-IP en modo de puerta de enlace integrada; en caso contrario devuelve el par del socket:
def client_ip_getter(request: Request) -> str:
if request.app.state.server_config.gateway_mode == GatewayModeEnum.embedded:
return request.headers.get("X-GPUStack-Real-IP", "")
else:
return request.client.host
La dependencia de autenticación de las líneas 89 a 92 promueve después un origen loopback al primer administrador cuando la opción es falsa:
if user is None and client_ip_getter(request=request) == "127.0.0.1":
if not server_config.force_auth_localhost:
user = await User.first_by_field(session, "is_admin", True)
La rama solo se ejecuta después de que las comprobaciones basic, cookie, bearer y clave de API no hayan producido un usuario. Sin credenciales, user es None, client_ip_getter devuelve 127.0.0.1 para una petición proxificada, y la consulta devuelve el administrador integrado. El código nunca comprueba el método HTTP, la ruta ni el llamante.
La misma función confía en una cabecera en modo de puerta de enlace integrada. Un cliente que establece X-GPUStack-Real-IP: 127.0.0.1 alcanza la misma rama sin proxy. El aviso documenta ambas vías.
¿Cómo se explota?
- El atacante envía una petición no autenticada a cualquier endpoint de la API a través del proxy inverso local.
- El proxy reenvía la petición a
127.0.0.1:<port>, así que GPUStack ve un par TCP loopback. get_current_userno encuentra credenciales, llama aclient_ip_getter, obtiene127.0.0.1y carga la cuenta de administrador integrada.- El atacante actúa ahora como administrador. El aviso enumera cambios de usuarios y modelos, lectura de secretos de workers y proveedores de nube, y creación de una clave de API de administrador persistente con alcance
*. - Con la clave de administrador, el atacante llama al endpoint reservado
POST /v2/inference-backends/from-yamly envía una definición de backend. Ese endpoint está diseñado para ejecutar backends de inferencia, y el aviso dice que los atacantes lo usaron para ejecutar shells inversas en los hosts workers. - El atacante conserva la clave de API de administrador, que sigue válida desde cualquier dirección de origen después de crearse. Eliminar la regla del proxy no la revoca.
El proveedor dice haber observado esta explotación en el mundo real contra un despliegue v2.1.1, con backends personalizados creados por el atacante que ejecutaban shells inversas. El aviso no da la fecha de esa observación ni el número de víctimas.
¿Cómo se desarrolla el ataque?
Un proxy inverso convierte la petición de red del atacante en una petición loopback, y la comprobación loopback la convierte en una sesión de administrador.
sin credenciales"] --> B["Proxy inverso local
par 127.0.0.1"] B --> C["get_current_user
sin usuario"] C --> D["client_ip_getter devuelve loopback
sin comprobar identidad"] D --> E["administrador integrado cargado"] E --> F["clave de API admin creada
alcance *"] F --> G["backend de inferencia desde YAML
shell inversa en workers"]
¿Qué puede hacer un atacante?
El impacto es el control administrativo completo del despliegue de GPUStack, y la ejecución de código en los hosts workers que gestiona.
El aviso enumera las acciones administrativas que puede realizar un atacante no autenticado:
- Enumerar y modificar usuarios, modelos, workers y configuración.
- Crear una clave de API de administrador persistente con alcance
*, válida desde cualquier dirección IP de origen después de crearse. - Leer credenciales de workers y secretos de proveedores de nube.
- Llamar al endpoint reservado
POST /v2/inference-backends/from-yamlpara ejecutar código en los hosts workers.
El atacante necesita accesibilidad de red a la API a través del proxy. El aviso dice que la explotación se observó contra un despliegue 2.1.1 con backends creados por el atacante que ejecutaban shells inversas. Eso es ejecución de código en los hosts workers, no solo en el servidor GPUStack.
Un despliegue cuya API solo es accesible desde una red de confianza sigue expuesto a cualquiera en esa red, incluido un puesto comprometido o un inquilino vecino.
¿Por qué funciona la corrección?
La corrección elimina por completo el atajo loopback, así que ninguna petición se autentica por su origen. También exige un token de puerta de enlace antes de confiar en la cabecera X-GPUStack-Real-IP.
El cambio de 2.2.0 en gpustack/api/auth.py borra las tres líneas que promovían a un llamante loopback:
@@ async def get_current_user(...)
if token is not None:
user, api_key = await get_user_from_api_token(session, token)
- if user is None and client_ip_getter(request=request) == "127.0.0.1":
- if not server_config.force_auth_localhost:
- user = await User.first_by_field(session, "is_admin", True)
if user:
if not user.is_active:
raise UnauthorizedException(message="User account is deactivated")
La misma versión reescribe client_ip_getter para que la cabecera de reenvío solo se lea después de que gateway_token_auth tenga éxito:
def client_ip_getter(request: Request) -> str:
- if request.app.state.server_config.gateway_mode == GatewayModeEnum.embedded:
- return request.headers.get("X-GPUStack-Real-IP", "")
- else:
- return request.client.host
+ if request.app.state.server_config.gateway_mode != GatewayModeEnum.disabled:
+ try:
+ gateway_token_auth(request)
+ real_ip = request.headers.get("X-GPUStack-Real-IP")
+ if real_ip:
+ return real_ip
+ except UnauthorizedException:
+ pass
+ return request.client.host if request.client else ""
La corrección es completa para esta omisión. No revoca las claves de API que un atacante creó antes de la actualización, y no limpia un host worker que ejecutó un backend enviado por el atacante. Eso necesita las comprobaciones de "¿Cómo compruebo si estuve comprometido?".
¿Cómo compruebo si estuve comprometido?
- Enumere los usuarios administradores y las claves de API en la interfaz de GPUStack. Un administrador o una clave que usted no creó son una pista; una clave con alcance
*creada fuera de su proceso de cambios es una prueba fuerte. - Enumere los backends de inferencia y compárelos con sus propios registros. La vía de explotación del aviso crea backends mediante
POST /v2/inference-backends/from-yaml, y el proveedor observó shells inversas desde ellos. - En los hosts workers, revise las conexiones salientes y los árboles de procesos por shells iniciadas por el proceso del backend de inferencia. Un backend que abre una conexión interactiva a una dirección externa es una pista.
- Busque en los registros del servidor GPUStack y del proxy peticiones de API que tuvieran éxito sin cabecera de autenticación. La rama vulnerable devuelve una respuesta administrativa normal, así que una petición exitosa sin credenciales es el patrón que hay que encontrar.
- Trate las credenciales de workers y proveedores de nube como leídas. Renuévelas y revise las cuentas de nube por recursos que usted no creó.
El aviso no da indicadores de compromiso ni número de víctimas. Una petición que falló antes de llegar a la rama vulnerable puede no dejar rastro.
Cronología
- 2026-05-19: el commit de corrección, "fix: remove localhost auto-admin bypass", llega al repositorio.
- 2026-06-18: se publica GPUStack 2.2.0 con la corrección.
- 2026-07-31: el proveedor publica el aviso GHSA-gj9m-f8ph-pmhr.
- 2026-09-29: se reserva el registro CVE y se actualiza el aviso.
- 2026-11-04: se publica CVE-2026-102895, con el informe de explotación en el mundo real.
Preguntas frecuentes
¿Está confirmada la explotación?
Sí. El aviso del proveedor dice que la vulnerabilidad "se ha observado explotada en el mundo real contra un despliegue v2.1.1", con backends personalizados creados por el atacante que ejecutaban shells inversas. El aviso no da fecha, número de víctimas ni indicadores.
¿Está afectada la configuración predeterminada?
Sí, cuando un proxy inverso local está delante de la API. force_auth_localhost es falso por defecto, y el proxy hace que el par TCP sea 127.0.0.1 para cada petición remota. Un despliegue directo sin proxy local no toma la vía loopback salvo que el modo de puerta de enlace integrada confíe en una cabecera falsificada.
¿La solución temporal del aviso lo corrige del todo?
No. Activar force_auth_localhost=true cierra la rama loopback en las versiones afectadas, que es la vía principal. La actualización elimina la rama y la confianza en la cabecera, así que es la corrección duradera.
¿Por qué la puntuación CVSS es 9.8?
El registro CVE la puntúa CVSS v3.1 9.8, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. El atacante no necesita privilegios ni interacción, y el impacto es alto en confidencialidad, integridad y disponibilidad.
¿Está en CISA KEV?
No. CVE-2026-102895 no está en el catálogo Known Exploited Vulnerabilities de CISA a fecha del 5 de noviembre de 2026. La declaración del proveedor sobre la explotación en el mundo real es la fuente.
¿La actualización revoca las claves que creó el atacante?
No. La actualización cierra la omisión pero deja intactos los usuarios y las claves de API existentes. Revoque las claves de administrador desconocidas y renueve las credenciales de workers y de nube después de actualizar.
Referencias
- Aviso de GPUStack GHSA-gj9m-f8ph-pmhr: https://github.com/gpustack/gpustack/security/advisories/GHSA-gj9m-f8ph-pmhr (comprobado el 5 de noviembre de 2026)
- Registro CVE-2026-102895: https://www.cve.org/CVERecord?id=CVE-2026-102895 (comprobado el 5 de noviembre de 2026)
- Commit de corrección de GPUStack 53f42a6a: https://github.com/gpustack/gpustack/commit/53f42a6a553542974396d16a4ff6b12bcbedb48d (comprobado el 5 de noviembre de 2026)
- Versión GPUStack v2.2.0: https://github.com/gpustack/gpustack/releases/tag/v2.2.0 (comprobado el 5 de noviembre de 2026)
- Código de GPUStack en v2.1.1,
gpustack/api/auth.py: https://github.com/gpustack/gpustack/blob/v2.1.1/gpustack/api/auth.py (comprobado el 5 de noviembre de 2026) - Repositorio de GPUStack: https://github.com/gpustack/gpustack (comprobado el 5 de noviembre de 2026)
- Docker Hub,
gpustack/gpustack: https://hub.docker.com/r/gpustack/gpustack (comprobado el 5 de noviembre de 2026)