GPUStack Auth Bypass RCE, Exploited in the Wild (CVE-2026-102895)
GPUStack before 2.2.0 trusts a loopback request source, so a same-host reverse proxy turns a remote request into admin access and code execution.
TL;DR
- GPUStack before 2.2.0 treats a request that appears to come from 127.0.0.1 as the built-in administrator, and a same-host reverse proxy makes every remote request look like loopback. An unauthenticated attacker then gets full administrative access.
- Every deployment earlier than 2.2.0 with the default
force_auth_localhost=falsebehind a same-host reverse proxy is exposed. The advisory also names a forgedX-GPUStack-Real-IPheader in embedded gateway mode. - The vendor reports exploitation in the wild against a 2.1.1 deployment, including attacker-created inference backends that ran reverse shells. The CVE record was published on November 4, 2026. It is not in CISA KEV as of November 5, 2026.
- Upgrade to GPUStack 2.2.0 or later and recreate the container. On older versions, set
force_auth_localhost=trueand strip client-suppliedX-GPUStack-Real-IPat the proxy. See "How do I fix it?".
Am I affected?
You are exposed when all of these conditions hold:
- Your GPUStack server runs a version before 2.2.0.
force_auth_localhostis false or unset, which is the default on those versions.- The API is reachable through a same-host reverse proxy configured with
proxy_pass http://127.0.0.1:<port>, so every remote request reaches GPUStack with TCP peer 127.0.0.1. In embedded gateway mode, an attacker who can setX-GPUStack-Real-IP: 127.0.0.1reaches the same branch.
The reverse-proxy condition is the shape the advisory calls the canonical self-hosted deployment: a same-host reverse proxy with proxy_pass http://127.0.0.1:<port>. That is exactly the arrangement the vulnerable branch trusted.
| Branch | Affected versions | Fixed version |
|---|---|---|
| 2.1.x and earlier | 2.1.1 and every earlier release | 2.2.0, released 2026-06-18 |
| 2.2.x | None (fixed in 2.2.0) | Keep updating to the latest release |
Check what you run:
# 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
The last command prints nothing when the setting is at its default. The setting also appears in the server configuration file as force_auth_localhost.
GPUStack is an open-source GPU cluster manager for AI model serving and GPU instance provisioning. Its Docker image had been pulled 942,824 times on Docker Hub on November 5, 2026, and the repository had 5,978 stars.
How do I fix it?
- Upgrade the server to GPUStack 2.2.0 or later:
pip install --upgrade "gpustack==2.2.0"
- Recreate the container with the new image. Pulling the image without recreating the container leaves the old process running.
- Confirm the loopback shortcut is gone. The
force_auth_localhostflag is a no-op in 2.2.0, and every request must authenticate. - Review the administrative state for changes you did not make. The advisory names user and model changes, persistent administrator API keys, worker and cloud-provider secrets, and inference backends created through the admin-only endpoint.
For deployments that cannot upgrade yet, the advisory gives three workarounds. They reduce exposure but do not remove the vulnerable code:
- Set
force_auth_localhost=trueso loopback requests also require authentication. - Strip any client-supplied
X-GPUStack-Real-IPheader at the reverse proxy or gateway, and make sure the proxy cannot make external traffic look like loopback. - Keep the API off untrusted networks, behind a VPN or edge authentication.
The first workaround is the effective one for the common reverse-proxy shape. The other two limit the second path and the exposure window.
What is the root cause?
The root cause is an authentication decision based on the request's network source. A local socket peer or a client-supplied header is not an identity, and a same-host reverse proxy makes the peer address meaningless.
This is the vulnerable code at v2.1.1, in gpustack/api/auth.py. The helper at lines 51 to 56 returns the X-GPUStack-Real-IP header only in embedded gateway mode; otherwise it returns the socket peer:
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
The authentication dependency at lines 89 to 92 then promotes a loopback source to the first administrator when the flag is false:
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)
The branch runs only after the basic, cookie, bearer and API-key checks all fail to produce a user. With no credentials at all, user is None, client_ip_getter returns 127.0.0.1 for a proxied request, and the query returns the built-in administrator. The code never checks the HTTP method, the path or the caller.
The same helper trusts a header in embedded gateway mode. A client that sets X-GPUStack-Real-IP: 127.0.0.1 reaches the same branch without a proxy. The advisory documents both paths.
How is it exploited?
- The attacker sends an unauthenticated request to any API endpoint through the same-host reverse proxy.
- The proxy forwards the request to
127.0.0.1:<port>, so GPUStack sees a loopback TCP peer. get_current_userfinds no credentials, callsclient_ip_getter, gets127.0.0.1, and loads the built-in administrator account.- The attacker now acts as an administrator. The advisory lists user and model changes, worker and cloud-provider secret reads, and creation of a persistent administrator API key with the
*scope. - With the admin key, the attacker calls the admin-only endpoint
POST /v2/inference-backends/from-yamland supplies a backend definition. That endpoint is designed to run inference backends, and the advisory says attackers used it to run reverse shells on worker hosts. - The attacker keeps the admin API key, which remains valid from any source address after creation. Removing the proxy rule does not revoke it.
The vendor says it observed this exploitation in the wild against a v2.1.1 deployment, with attacker-created custom backends running reverse shells. The advisory does not give the date of that observation or the number of victims.
How does the attack flow?
A reverse proxy turns the attacker's network request into a loopback request, and the loopback check turns it into an administrator session.
no credentials"] --> B["Same-host reverse proxy
peer 127.0.0.1"] B --> C["get_current_user
no user found"] C --> D["client_ip_getter returns loopback
no identity check"] D --> E["built-in administrator loaded"] E --> F["Admin API key created
scope *"] F --> G["inference backend from YAML
reverse shell on workers"]
What can an attacker do?
The impact is full administrative control of the GPUStack deployment, and code execution on the worker hosts it manages.
The advisory lists the administrative actions an unauthenticated attacker can take:
- Enumerate and modify users, models, workers and configuration.
- Create a persistent administrator API key with the
*scope, valid from any source IP after creation. - Read worker credentials and cloud-provider secrets.
- Call the admin-only
POST /v2/inference-backends/from-yamlendpoint to run code on worker hosts.
The attacker needs network reachability to the API through the proxy. The advisory says exploitation was observed against a 2.1.1 deployment with attacker-created backends running reverse shells. That is code execution on the worker hosts, not only on the GPUStack server.
A deployment whose API is reachable only from a trusted network is still exposed to anyone on that network, including a compromised workstation or a co-tenant.
Why does the fix work?
The fix removes the loopback shortcut entirely, so no request is authenticated by its source. It also requires a gateway token before the X-GPUStack-Real-IP header is trusted.
The 2.2.0 change in gpustack/api/auth.py deletes the three lines that promoted a loopback caller:
@@ 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")
The same release rewrites client_ip_getter so the forwarding header is read only after gateway_token_auth succeeds:
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 ""
The fix is complete for this bypass. It does not revoke API keys an attacker created before the upgrade, and it does not clean a worker host that ran an attacker-supplied backend. Those need the checks in "How do I check if I was compromised?".
How do I check if I was compromised?
- List the administrator users and API keys in the GPUStack UI. An administrator or a key you did not create is a lead; a key with the
*scope created outside your change process is strong evidence. - List the inference backends and compare them with your own records. The advisory's exploitation path creates backends through
POST /v2/inference-backends/from-yaml, and the vendor observed reverse shells from them. - On the worker hosts, review outbound connections and process trees for shells started by the inference backend process. A backend that opens an interactive connection to an external address is a lead.
- Search the GPUStack server and proxy logs for API requests that succeeded without an authentication header. The vulnerable branch returns a normal administrative response, so a successful request with no credentials is the pattern to find.
- Treat the worker and cloud-provider credentials as read. Rotate them and review the cloud accounts for resources you did not create.
The advisory gives no indicators of compromise and no victim count. A request that failed before reaching the vulnerable branch may leave no trace.
Timeline
- 2026-05-19: the fix commit, "fix: remove localhost auto-admin bypass", lands in the repository.
- 2026-06-18: GPUStack 2.2.0 is released with the fix.
- 2026-07-31: the vendor publishes advisory GHSA-gj9m-f8ph-pmhr.
- 2026-09-29: the CVE record is reserved and the advisory is updated.
- 2026-11-04: CVE-2026-102895 is published, including the report of in-the-wild exploitation.
FAQ
Is exploitation confirmed?
Yes. The vendor advisory says the vulnerability "has been observed exploited in the wild against a v2.1.1 deployment", with attacker-created custom backends running reverse shells. The advisory does not give a date, a victim count or indicators.
Is the default configuration affected?
Yes, when a same-host reverse proxy fronts the API. force_auth_localhost defaults to false, and the proxy makes the TCP peer 127.0.0.1 for every remote request. A direct deployment without a same-host proxy does not take the loopback path unless embedded gateway mode trusts a forged header.
Does the advisory's workaround fully fix it?
No. Setting force_auth_localhost=true closes the loopback branch on affected versions, which is the main path. The upgrade removes the branch and the header trust, so it is the durable fix.
Why is the CVSS score 9.8?
The CVE record scores it CVSS v3.1 9.8, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. The attacker needs no privileges or user interaction, and the impact is high on confidentiality, integrity and availability.
Is it in CISA KEV?
No. CVE-2026-102895 is not in the CISA Known Exploited Vulnerabilities catalog as of November 5, 2026. The vendor's in-the-wild statement is the exploitation source.
Does upgrading revoke keys the attacker created?
No. The upgrade closes the bypass but leaves existing users and API keys in place. Revoke unknown administrator keys and rotate the worker and cloud credentials after upgrading.
References
- GPUStack advisory GHSA-gj9m-f8ph-pmhr: https://github.com/gpustack/gpustack/security/advisories/GHSA-gj9m-f8ph-pmhr (checked November 5, 2026)
- CVE-2026-102895 record: https://www.cve.org/CVERecord?id=CVE-2026-102895 (checked November 5, 2026)
- GPUStack fix commit 53f42a6a: https://github.com/gpustack/gpustack/commit/53f42a6a553542974396d16a4ff6b12bcbedb48d (checked November 5, 2026)
- GPUStack v2.2.0 release: https://github.com/gpustack/gpustack/releases/tag/v2.2.0 (checked November 5, 2026)
- GPUStack source at v2.1.1,
gpustack/api/auth.py: https://github.com/gpustack/gpustack/blob/v2.1.1/gpustack/api/auth.py (checked November 5, 2026) - GPUStack repository: https://github.com/gpustack/gpustack (checked November 5, 2026)
- Docker Hub,
gpustack/gpustack: https://hub.docker.com/r/gpustack/gpustack (checked November 5, 2026)