| 研究者 | Dostxodjayev Abdullox (@squeeze440) |
| アドバイザリ | GHSA-5293-fcm6-fh8v |
| CVE | CVE-2026-87009 |
| CVSS 3.1 | 5.4 (Medium) — CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L |
| 脆弱性 | CWE-352, CWE-306, CWE-346 |
| ステータス | v0.46.0 で修正済み |
概要: inference-gateway は 0.0.0.0 にバインドし、デフォルトで認証を無効 (AUTH_ENABLED=false) にして出荷されており、その ANY /proxy/:provider/*path パススルールートは、呼び出し元が指定した Authorization ヘッダーを無条件に削除し、上流へ転送する前にゲートウェイ運用者自身がサーバーに設定したプロバイダー API キーへ置き換えます。CORS ポリシーも CSRF 保護も一切存在しないため、被害者のブラウザが訪問するあらゆる Web ページが、被害者自身の OpenAI/Anthropic などのアカウントを通じて、課金対象の LLM リクエストを無言で実行させることができます。
製品: inference-gateway/inference-gateway — セルフホスト型、クラウドネイティブな LLM ゲートウェイ (Go, Gin)。
テスト済みバージョン: コミット 6677da6afd0a606899f833c2351635edeef386f5 (main, 2026-08-04)。影響範囲: <= 0.45.0。
3 つの独立した事実が組み合わさってこのバグになります。
1. 認証はオフで、バインドはデフォルトで公開されている。
config/config.go:77 — AuthConfig.Enabled のデフォルトは false。config/config.go:94 — ServerConfig.Host のデフォルトは 0.0.0.0。認証が無効の場合、NewOIDCAuthenticatorMiddleware (api/middlewares/auth.go:27-30) は OIDCAuthenticatorNoop を返し、その Middleware() (api/middlewares/auth.go:48-52) は純粋なパススルーです。このモードでは、どのルートにもリクエストごとの ID チェックはありません。クイックスタートの examples/docker-compose/basic/docker-compose.yml は AUTH_ENABLED を設定せずに 8080:8080 を公開しているため、ドキュメント化された入門手順はまさにこの構成を生成します。
2. /proxy/:provider/*path は常に運用者自身のプロバイダーキーを注入する。
api/routes.go:102-131 (ProxyHandler) は applyProviderAuth を呼び出します (api/routes.go:287-312):
func applyProviderAuth(req *http.Request, provider core.IProvider) error {
req.Header.Del("Authorization") // caller's own Authorization header is discarded
token := provider.GetToken() // the operator's configured key (env var, e.g. OPENAI_API_KEY)
switch provider.GetAuthType() {
case constants.AuthTypeBearer:
req.Header.Set("Authorization", "Bearer "+token)
...
呼び出し元自身の資格情報が使用されるコードパスは存在せず、この設計は常にゲートウェイに設定されたキーを代用します。事実 1 と組み合わせると、認証されていない呼び出し元が運用者の実際のキーを無償で付与されることになります。
3. CORS ポリシーも CSRF 保護もミドルウェアチェーンのどこにも存在しない。
cmd/gateway/main.go:271 は gin.New() (デフォルトミドルウェアなし) でルーターを構築します。チェーン (:273-290) は otel → logger → telemetry → OIDC auth → guardrails → MCP です。go.mod/go.sum には CORS パッケージが含まれていません。Access-Control-Allow-Origin ヘッダーは一切送信されません。プロキシハンドラーは動作するためにカスタムヘッダーや CORS アンセーフな Content-Type を必要としません (生のボディを転送し、その後 api/routes.go:254 で送信側の Content-Type を application/json に上書きします)。そのため、「単純な」クロスオリジン fetch() (Content-Type: text/plain、カスタムヘッダーなし) はブラウザによってプリフライトなしで送信されます。サーバー側の課金対象リクエストは、ブラウザの読み取り側 CORS 強制とは独立して完了します。
正味の影響: 被害者のブラウザが訪問するあらゆるオリジンが、そのブラウザからゲートウェイに到達可能である限り (ループバック、LAN、または運用者がドキュメント化された 8080:8080 公開パターンに従った場合はパブリック)、認証ゼロ、ユーザーに可視の兆候もなく、運用者の実際のプロバイダーアカウントを通じて攻撃者が選択した任意のチャット補完を実行させることができます。
実際の Chrome ブラウザを使用し、2 つの異なるループバックオリジン間 (ゲートウェイは 127.0.0.1、攻撃者ページは 127.0.0.2) で本物のクロスオリジンリクエストを行うことで、エンドツーエンドで動的に確認しました。poc/ を参照してください:
poc/attacker_site/attack.html — 攻撃者オリジンから提供される正確なページ。読み込み時の唯一のアクションは /proxy/openai/chat/completions への 1 回の fetch() です。poc/mock_upstream.py — api.openai.com の代役で、受け取った Authorization ヘッダー、Origin、ボディをログに記録します。poc/README.md — 完全な実行手順。観測結果: クロスオリジンのブラウザリクエスト (Origin: http://127.0.0.2:8000) がモック上流に到達し、Authorization: Bearer sk-proj-VICTIM-REAL-BILLED-KEY-... と攻撃者が選択したボディ {"messages":[{"role":"user","content":"CSRF-DRIVEBY-MARKER-8271"}]} を運んでいました。攻撃者ページは資格情報を一切所持せず、見ることも要求されることもありませんでした。ブラウザのネットワーク検査により、127.0.0.2:8000 のページから POST http://127.0.0.1:8081/proxy/openai/chat/completions [200] が発火したことが確認されました。完全なブラウザ駆動の証拠 (ネットワークログ、スクリーンショット) は GHSA-5293-fcm6-fh8v に添付されています。
ドキュメント化されたデフォルト構成で inference-gateway を実行している運用者は、ゲートウェイのポートに到達できるブラウザから到達可能なあらゆる Web ページによって、設定されたプロバイダー API キーを使用可能にされます。必要なのは「ゲートウェイのアドレスに HTTP リクエストを送信できる」ことだけで、資格情報、Cookie、特別なネットワーク位置は不要です。具体的には、運用者自身のプロバイダーアカウントにおける不正な課金/クォータ消費が、ゲートウェイの実行中に運用者 (または同じ LAN 上の誰か) が開いている任意のサードパーティ Web サイト、広告、侵害されたページによって無差別に引き起こされます。ブラウザは攻撃者がモデル出力を読むことをブロックするため (CORS ヘッダーなし)、これは読み取りプリミティブではなく、ブラインドの強制トランザクションです。
Origin/Sec-Fetch-Site チェック、CORS 制限のいずれもなしに、ブラウザ発行のクロスオリジンリクエストを介して実行される高コストな状態変更アクション。AUTH_ENABLED=false (ドキュメント化されたデフォルト) の場合、/health を除くすべてのルートにリクエストごとの ID チェックがありません。v0.46.0 で修正済み (メンテナーがデフォルトを強化)。推奨される対策:
/proxy/:provider/*path (およびその他の状態変更ルート) に、カスタムの非セーフリストヘッダーを要求する。これによりクロスオリジン呼び出し元に対して CORS プリフライトが強制され、ゲートウェイがオリジン許可リストを強制する場所を得られます。これにより、認証を有効化する必要なしに「単純リクエスト」バイパスを塞ぐことができます。SERVER_HOST のデフォルトを 0.0.0.0 から 127.0.0.1 に変更し、より広いインターフェースへの明示的なオプトインを要求する (Ollama が同種のバグに対して行ったように)。AUTH_ENABLED=false かつ SERVER_HOST がループバックでない場合、起動時の警告を出力するか、起動を拒否する。README.md / Configurations.md にリスクを記載する。Dostxodjayev Abdullox (@squeeze440)