GPUStackの認証バイパスによるRCE、実環境で悪用 (CVE-2026-102895)
GPUStack 2.2.0より前はループバック由来の要求を信頼するため、同一ホストのリバースプロキシが遠隔要求を管理者権限とコード実行に変える。
要点
- GPUStack 2.2.0より前は、127.0.0.1から来たように見える要求を組み込み管理者として扱う。同一ホストのリバースプロキシは、すべての遠隔要求をループバックに見せる。未認証の攻撃者が完全な管理者権限を得る。
- 既定の
force_auth_localhost=falseで同一ホストのリバースプロキシの背後にある2.2.0より前の展開はすべて影響を受ける。勧告は組み込みゲートウェイモードでの偽造X-GPUStack-Real-IPヘッダーも挙げている。 - ベンダーは2.1.1の展開に対する実環境での悪用を報告し、攻撃者が作成した推論バックエンドがリバースシェルを実行した。CVEレコードは2026年11月4日に公開された。2026年11月5日時点でCISA KEVにはない。
- GPUStack 2.2.0以降に更新し、コンテナを再作成する。古いバージョンでは
force_auth_localhost=trueを設定し、プロキシでクライアント提供のX-GPUStack-Real-IPを削除する。「修正方法」を参照のこと。
影響を受けますか?
次の条件がすべて当てはまる場合に影響を受けます。
- GPUStackサーバーが2.2.0より前のバージョンを実行している。
force_auth_localhostが偽または未設定である。これらのバージョンの既定値である。- APIが
proxy_pass http://127.0.0.1:<port>で設定された同一ホストのリバースプロキシ経由で到達可能で、すべての遠隔要求がTCPピア127.0.0.1でGPUStackに届く。組み込みゲートウェイモードでは、X-GPUStack-Real-IP: 127.0.0.1を設定できる攻撃者が同じ分岐に到達する。
リバースプロキシの条件は、勧告が標準的な自己ホスト展開と呼ぶ構成である。同一ホストのリバースプロキシとproxy_pass http://127.0.0.1:<port>の組み合わせで、まさに脆弱な分岐が信頼していた構成である。
| 系列 | 影響を受けるバージョン | 修正版 |
|---|---|---|
| 2.1.x以前 | 2.1.1およびそれ以前のすべて | 2.2.0(2026-06-18公開) |
| 2.2.x | なし(2.2.0で修正済み) | 最新リリースへの更新を続ける |
実行内容を確認する。
# 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
最後のコマンドは設定が既定値のとき何も表示しない。設定はサーバー設定ファイルにもforce_auth_localhostとして現れる。
GPUStackはAIモデル配信とGPUインスタンスのプロビジョニング向けのオープンソースGPUクラスタマネージャーである。Dockerイメージは2026年11月5日時点でDocker Hubで942,824回取得され、リポジトリのスターは5,978だった。
修正方法
- サーバーをGPUStack 2.2.0以降に更新する。
pip install --upgrade "gpustack==2.2.0"
- 新しいイメージでコンテナを再作成する。イメージを取得してもコンテナを再作成しなければ、古いプロセスが動き続ける。
- ループバックの近道が消えたことを確認する。
force_auth_localhostフラグは2.2.0では無効で、すべての要求が認証を必要とする。 - 管理者状態に自分が行っていない変更がないか確認する。勧告はユーザーとモデルの変更、永続的な管理者APIキー、ワーカーとクラウドプロバイダーの秘密情報、管理者専用エンドポイントで作成された推論バックエンドを挙げている。
まだ更新できない展開には、勧告が3つの回避策を示している。露出は減るが、脆弱なコードは残る。
force_auth_localhost=trueを設定し、ループバック要求にも認証を要求する。- リバースプロキシまたはゲートウェイで、クライアント提供の
X-GPUStack-Real-IPヘッダーを削除し、プロキシが外部トラフィックをループバックに見せかけられないようにする。 - APIを信頼できないネットワークの外に置き、VPNまたはエッジ認証の背後に保つ。
最初の回避策が通常のリバースプロキシ構成に有効である。残りの2つは2番目の経路と露出期間を制限する。
根本原因は何ですか?
根本原因は、要求のネットワーク送信元に基づく認証判断である。ローカルのソケットピアやクライアント提供のヘッダーは身元ではなく、同一ホストのリバースプロキシはピアアドレスを無意味にする。
これがv2.1.1のgpustack/api/auth.pyにある脆弱なコードである。51行目から56行目のヘルパーは、組み込みゲートウェイモードでのみX-GPUStack-Real-IPヘッダーを返し、それ以外ではソケットピアを返す。
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
89行目から92行目の認証依存関係は、フラグが偽のときループバックの送信元を最初の管理者に昇格させる。
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)
この分岐は、basic、cookie、bearer、APIキーの各チェックがすべてユーザーを返さなかった後にだけ実行される。認証情報がまったくない場合、userはNoneで、プロキシ経由の要求ではclient_ip_getterが127.0.0.1を返し、クエリが組み込み管理者を返す。コードはHTTPメソッド、パス、呼び出し元を確認しない。
同じヘルパーは組み込みゲートウェイモードでヘッダーを信頼する。X-GPUStack-Real-IP: 127.0.0.1を設定するクライアントは、プロキシなしで同じ分岐に到達する。勧告は両方の経路を記録している。
どのように悪用されますか?
- 攻撃者が同一ホストのリバースプロキシ経由で任意のAPIエンドポイントに未認証要求を送る。
- プロキシが要求を
127.0.0.1:<port>に転送するため、GPUStackはループバックのTCPピアを見る。 get_current_userは認証情報を見つけず、client_ip_getterを呼び、127.0.0.1を得て組み込み管理者アカウントを読み込む。- 攻撃者は管理者として行動する。勧告はユーザーとモデルの変更、ワーカーとクラウドプロバイダーの秘密情報の読み取り、
*スコープの永続的な管理者APIキーの作成を挙げている。 - 管理者キーで、攻撃者は管理者専用の
POST /v2/inference-backends/from-yamlを呼び、バックエンド定義を送る。このエンドポイントは推論バックエンドを実行するためのもので、勧告によると攻撃者はこれを使ってワーカーホストでリバースシェルを実行した。 - 攻撃者は管理者APIキーを保持する。作成後は任意の送信元アドレスから有効である。プロキシのルールを削除しても失効しない。
ベンダーは、攻撃者が作成したカスタムバックエンドがリバースシェルを実行した、v2.1.1展開に対する実環境での悪用を観測したと述べている。勧告はその観測の日付や被害者数を示していない。
攻撃の流れは?
リバースプロキシが攻撃者のネットワーク要求をループバック要求に変え、ループバックチェックがそれを管理者セッションに変える。
認証情報なし"] --> B["同一ホストのプロキシ
ピア127.0.0.1"] B --> C["get_current_user
ユーザーなし"] C --> D["client_ip_getterがループバックを返す
身元確認なし"] D --> E["組み込み管理者を読み込み"] E --> F["管理者APIキー作成
スコープ*"] F --> G["YAMLから推論バックエンド
ワーカーでリバースシェル"]
攻撃者に何ができますか?
影響はGPUStack展開の完全な管理者制御と、それが管理するワーカーホストでのコード実行である。
勧告は未認証の攻撃者が実行できる管理者操作を列挙している。
- ユーザー、モデル、ワーカー、設定を列挙して変更する。
- 作成後は任意の送信元IPから有効な、
*スコープの永続的な管理者APIキーを作成する。 - ワーカーの認証情報とクラウドプロバイダーの秘密情報を読む。
- 管理者専用の
POST /v2/inference-backends/from-yamlを呼び、ワーカーホストでコードを実行する。
攻撃者にはプロキシ経由でAPIに到達できる必要がある。勧告は、攻撃者が作成したバックエンドがリバースシェルを実行した2.1.1展開に対する悪用を観測したと述べている。これはGPUStackサーバーだけでなく、ワーカーホストでのコード実行である。
APIが信頼できるネットワークからのみ到達可能な展開も、そのネットワーク上の誰にでも露出する。侵害されたワークステーションや隣接テナントを含む。
修正が機能する理由
修正はループバックの近道を完全に削除するため、どの要求も送信元で認証されない。また、X-GPUStack-Real-IPヘッダーを信頼する前にゲートウェイトークンを要求する。
2.2.0のgpustack/api/auth.pyの変更は、ループバックの呼び出し元を昇格させていた3行を削除する。
@@ 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")
同じリリースはclient_ip_getterを書き換え、gateway_token_authが成功した後にだけ転送ヘッダーを読む。
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 ""
修正はこのバイパスに対して完全である。更新前に攻撃者が作成したAPIキーを失効させず、攻撃者が提供したバックエンドを実行したワーカーホストを清掃しない。それには「侵害されたかどうかを確認するには?」の確認が必要である。
侵害されたかどうかを確認するには?
- GPUStackの画面で管理者ユーザーとAPIキーを一覧する。自分が作成していない管理者やキーは手がかりであり、変更手続きの外で作成された
*スコープのキーは強い証拠である。 - 推論バックエンドを一覧し、自分の記録と比較する。勧告の悪用経路は
POST /v2/inference-backends/from-yamlでバックエンドを作成し、ベンダーはそこからリバースシェルを観測した。 - ワーカーホストで、推論バックエンドのプロセスが起動したシェルがないか、外向き接続とプロセスツリーを確認する。外部アドレスへの対話接続を開くバックエンドは手がかりである。
- GPUStackサーバーとプロキシのログで、認証ヘッダーなしで成功したAPI要求を探す。脆弱な分岐は通常の管理者応答を返すため、認証情報なしで成功した要求が探すべきパターンである。
- ワーカーとクラウドプロバイダーの認証情報は読まれたものとして扱う。更新し、クラウドアカウントで自分が作成していないリソースを確認する。
勧告は侵害指標も被害者数も示していない。脆弱な分岐に到達する前に失敗した要求は痕跡を残さないことがある。
年表
- 2026-05-19: 修正コミット「fix: remove localhost auto-admin bypass」がリポジトリに入る。
- 2026-06-18: 修正を含むGPUStack 2.2.0が公開される。
- 2026-07-31: ベンダーが勧告GHSA-gj9m-f8ph-pmhrを公開する。
- 2026-09-29: CVEレコードが予約され、勧告が更新される。
- 2026-11-04: CVE-2026-102895が公開され、実環境での悪用報告が含まれる。
よくある質問
悪用は確認されていますか?
はい。ベンダーの勧告は、脆弱性が「v2.1.1の展開に対して実環境で悪用されるのが観測された」と述べ、攻撃者が作成したカスタムバックエンドがリバースシェルを実行したとしています。勧告は日付、被害者数、指標を示していません。
既定の設定は影響を受けますか?
はい、同一ホストのリバースプロキシがAPIの前にある場合です。force_auth_localhostの既定は偽で、プロキシはすべての遠隔要求のTCPピアを127.0.0.1にします。同一ホストのプロキシがない直接の展開は、組み込みゲートウェイモードが偽造ヘッダーを信頼しない限りループバック経路を取りません。
勧告の回避策で完全に修正できますか?
いいえ。force_auth_localhost=trueを設定すると影響を受けるバージョンのループバック分岐が閉じ、これが主な経路です。更新は分岐とヘッダーの信頼を削除するため、恒久的な修正です。
なぜCVSSスコアは9.8ですか?
CVEレコードはCVSS v3.1 9.8、CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:Hと評価しています。攻撃者に特権も操作も不要で、機密性、完全性、可用性への影響が高いためです。
CISA KEVにありますか?
いいえ。CVE-2026-102895は2026年11月5日時点でCISAのKnown Exploited Vulnerabilitiesカタログにありません。ベンダーの実環境悪用の声明が悪用の出典です。
更新すると攻撃者が作成したキーは失効しますか?
いいえ。更新はバイパスを閉じますが、既存のユーザーとAPIキーは残ります。不明な管理者キーを失効させ、更新後にワーカーとクラウドの認証情報を更新してください。
参考資料
- GPUStack勧告GHSA-gj9m-f8ph-pmhr: https://github.com/gpustack/gpustack/security/advisories/GHSA-gj9m-f8ph-pmhr (2026年11月5日確認)
- CVE-2026-102895の記録: https://www.cve.org/CVERecord?id=CVE-2026-102895 (2026年11月5日確認)
- GPUStack修正コミット53f42a6a: https://github.com/gpustack/gpustack/commit/53f42a6a553542974396d16a4ff6b12bcbedb48d (2026年11月5日確認)
- GPUStack v2.2.0リリース: https://github.com/gpustack/gpustack/releases/tag/v2.2.0 (2026年11月5日確認)
- v2.1.1のGPUStackソース、
gpustack/api/auth.py: https://github.com/gpustack/gpustack/blob/v2.1.1/gpustack/api/auth.py (2026年11月5日確認) - GPUStackリポジトリ: https://github.com/gpustack/gpustack (2026年11月5日確認)
- Docker Hub、
gpustack/gpustack: https://hub.docker.com/r/gpustack/gpustack (2026年11月5日確認)