DEV Community

Cover image for GPUStack 2.1.1: one header = full admin, no login
frank chu
frank chu

Posted on

GPUStack 2.1.1: one header = full admin, no login

GPUStack 2.1.1: one header = full admin, no login

One HTTP header turned a remote, unauthenticated request into full admin on GPUStack 2.1.1. CISA added it to the Known Exploited Vulnerabilities catalog on November 9, 2026, with a November 12 patch deadline for federal agencies — after attackers used it against a real 2.1.1 deployment. I reproduced the whole chain locally: the bypass, the persistence, and the fix.

The bug, in 12 lines

GPUStack is an open-source GPU cluster manager for serving AI models. Its auth lives in gpustack/api/auth.py. The function that decides who you are, get_current_user, ends with this:

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
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode

Two design decisions stack into the vulnerability. First, a request that appears to come from loopback is treated as the built-in admin — no password, no token, no cookie. That's meant for the local CLI and the gateway on the same box. Second, in embedded gateway mode, "appears to come from loopback" means "sent the header X-GPUStack-Real-IP: 127.0.0.1" — a client-supplied value, trusted verbatim. And force_auth_localhost, the flag that would disable the loopback trust, defaults to False.

The advisory (GHSA-gj9m-f8ph-pmhr, CVE-2026-102895, CVSS 9.8) says exploitation was observed against a 2.1.1 deployment, where attackers created custom inference backends that opened reverse shells on worker hosts. The full impact list: user and model changes, persistent admin API-key creation, disclosure of worker and cloud-provider secrets, and code execution through POST /v2/inference-backends/from-yaml.

The lab

I installed GPUStack 2.1.1 from PyPI on this VM (no GPU — the API server doesn't need one) with a local PostgreSQL 16 for its database, and ran it two ways:

  • Real binary, --gateway-mode disabled: the API listens directly on 0.0.0.0:30080. In this mode client_ip_getter uses the TCP peer address, so it tests the loopback-trust branch in its simplest form.
  • The same app object with gateway_mode="embedded": the full embedded start needs a live Kubernetes API server for its Higress gateway, which this VM doesn't have, so I served the real create_app() with the same Config and the same database on port 30081. The auth function under test only reads cfg.gateway_mode, so this exercises the exact production code path, minus the gateway process that would normally set the header from the TCP peer.

"Remote" in this sandbox means a second loopback source address, 127.0.0.2 — the sandbox blocks outbound TCP to the VM's external IP, so I bound the client socket to 127.0.0.2 instead. The server sees the source address, which is the only thing the check looks at. Getting the 2.1.1 binary running took some shimming unrelated to the bug: two of its database migrations assume psycopg2 parameter typing and fail under psycopg3 (I patched the migration SQL to equivalent statements), and its pinned-era FastAPI call needs fastapi==0.128.0 because newer FastAPI removed response_model_exclude_unset. None of that touches auth.py.

The results

mode        source       header                          endpoint        result
disabled    127.0.0.1    —                               GET /v2/users   200 admin user list
disabled    127.0.0.2    —                               GET /v2/users   401 Invalid authentication credentials
disabled    127.0.0.2    X-GPUStack-Real-IP: 127.0.0.1  GET /v2/users   401 (header ignored in this mode)
embedded    127.0.0.2    —                               GET /v2/users   401
embedded    127.0.0.2    X-GPUStack-Real-IP: 127.0.0.1  GET /v2/users   200 admin user list
embedded    127.0.0.2    X-GPUStack-Real-IP: 127.0.0.2  GET /v2/users   401 (only the loopback value works)
Enter fullscreen mode Exit fullscreen mode

The loopback branch is exactly as broad as the code says, and exactly as narrow: only the literal string 127.0.0.1 unlocks it. The disabled-mode rows are the control group — same binary, same database, same default force_auth_localhost=False — and they show the header only becomes a key when the gateway is embedded, which is the default deployment shape.

Persistence: the key outlives the trick

Admin access for one request is bad. What makes it a real incident is that admin can mint credentials that don't depend on the trick at all. From loopback, with no auth of any kind, I created an API key:

POST /v2/api-keys  {"name": "persistence-demo"}
-> 200, id 1, value gpustack_d6da8176… (truncated), expires_at: null
Enter fullscreen mode Exit fullscreen mode

No expiry. Then I used that key from the "remote" source address, both as a Bearer token and as X-API-Key — both returned the full admin user list. The loopback moment is only needed once; after that the attacker holds a normal admin credential that works from anywhere, and nothing in the server distinguishes it from a key the admin created on purpose. The advisory's remediation note says the same thing: after an exposed deployment, audit issued keys, proxy trust settings, and worker secrets — patching alone doesn't revoke what was already minted.

I did not execute the from-yaml backend step — creating a backend that runs code on a worker is the RCE the advisory describes, and the admin API key I already held is sufficient proof that the gate is open. The bypass is the story; the payload is the advisory's.

The fix: 2.2.0

I verified the fix the same way I verified the bug — live, against 2.2.0, with the same embedded-mode harness. Two changes in the diff:

  1. The loopback-admin block is gone from get_current_user entirely. There is no longer any code path that turns "looks local" into "is admin."
  2. client_ip_getter only reads the X-GPUStack-Real-IP header after gateway_token_auth succeeds — the request must carry X-GPUStack-Auth-Token matching the server's derived gateway token. A client-supplied header without the gateway token is ignored, and the function falls back to the TCP peer address.

The numbers:

2.2.0 embedded, loopback 127.0.0.1, no auth:              401
2.2.0 embedded, remote + X-GPUStack-Real-IP: 127.0.0.1:   401
2.2.0 embedded, loopback + real admin basic auth:         200
Enter fullscreen mode Exit fullscreen mode

Same harness, same database shape, same default settings — the two requests that returned the full admin user list on 2.1.1 now return 401, and legitimate admin credentials still work. I also confirmed on the real 2.2.0 binary (disabled gateway mode): unauthenticated loopback is 401, admin basic auth is 200. The fix is real, not a config accident.

What to do if you run GPUStack

  • Upgrade to 2.2.0 or later. That is the whole fix.
  • If you can't upgrade today, start with --force-auth-localhost — it closes the loopback-trust branch entirely.
  • Then audit: list API keys (GET /v2/api-keys) and users, look for keys you didn't create, rotate worker credentials and cloud-provider secrets, and check whether any inference backend was created from YAML you don't recognize.
  • Don't rely on "it's behind a reverse proxy" as the mitigation. The proxy is the attack surface here: the bug is specifically that the application trusts what the proxy tells it about the client.

The uncomfortable part isn't the header. It's the default: a fresh install, with no configuration change, treats "I claim to be local" as "I am the admin." The header is just the shortest way to make the claim.

Sources: GitHub Security Advisory GHSA-gj9m-f8ph-pmhr (GPUStack improper authentication, fixed in 2.2.0, commit c6e6b91); CVE-2026-102895 (CVSS 3.1 9.8, published November 4, 2026); CISA Known Exploited Vulnerabilities catalog entry, added November 9, 2026, due November 12, 2026; The Hacker News, "CISA Warns of Active Exploitation of OpenSSH, GPUStack, FreeSWITCH, and IBM Flaws," November 10, 2026. All requests above were made against a local throwaway GPUStack 2.1.1 on this VM; the created API key was truncated in this article and the instance was torn down afterward. The embedded-mode test served the real application object with gateway_mode="embedded" and the Higress gateway process absent; the X-GPUStack-Real-IP header is the value the gateway normally sets from the TCP peer.

Top comments (0)