flawopen.com/Teardowns/cve-2026-71416-headroom-websocket-origin-hijacking
CVE-2026-71416:HeadroomのWebSocket Origin乗っ取り
HeadroomはWebSocketのOriginを検証せず、閲覧したサイトからプロキシ経由でLLM要求を中継できました。0.35.0以降へ更新してください。
受付係が訪問者の身分証を確かめず、誰でも専用窓口に通してしまう状態です。Originはその身分証に当たります。保存済みAPIキーを使える接続を開く前に、プロキシが確認しなければなりません。
主要な概念と専門用語
WebSocket- HTTP Upgrade要求から始まり、双方向に継続してメッセージをやり取りする接続です。
Originヘッダー- 要求を開始したページを示すため、ブラウザーが送るスキーム、ホスト、ポートです。WebSocketハンドシェイクではサーバー自身の信頼ポリシーが必要で、CORSヘッダーはその確認の代わりになりません。
CSWSH- Cross-Site WebSocket Hijacking。不審なページが到達可能なサービスへWebSocketを開き、サービスが接続元を確認しないことで成立します。
暗黙に利用可能な資格情報- 環境変数のAPIキーのように、プロセスが既に利用でき、クライアント自身の資格情報がない要求にも使われ得る秘密情報です。
CWE-1385- WebSocketでOrigin検証が欠落している弱点です。エンドポイントが通信元を許可済みか確認しません。
フレームワーク対応
- このガイドで説明する弱点の CWE 参照: CWE-1385
- OWASP GenAI 2025 — LLM06:2025 過剰なエージェンシー
根本原因の分析 (Root Cause)
Headroomの/v1/responses WebSocketハンドラーは、ハンドシェイクのOriginを確認する前にブラウザー接続を受け入れていました。その後、プロキシは自身の環境変数からOPENAI_API_KEYを上流要求に加えることができました。ブラウザーの同一オリジンポリシーだけでは、JavaScriptによる別の到達可能なホストへのWebSocket開始を防げません。ブラウザーは通常Originを送信するため、サーバー側でポリシー外のOriginを拒否する必要があります。したがって、被害者のブラウザーから脆弱なHeadroomプロキシへ到達でき、プロキシに有効な資格情報または連携機能があることが条件です。アドバイザリによると、受け入れられた接続から任意のLLM要求を送信できます。ホスト上のコマンド実行は、要求がコマンドを実行する設定済みツールを使える場合に限られ、ヘッダー検証の欠落だけで自動的に起きるものではありません。CNAは0.35.0未満を影響範囲とし、GitHubアドバイザリのパッケージメタデータは0.23.0を挙げています。0.35.0以降へ更新し、インストール済みの版を確認してください。
ステップ・バイ・ステップの攻撃フロー
プロキシが上流への権限を保持
HeadroomプロセスはOPENAI_API_KEYにOpenAIキーを保持する場合があります。ブラウザークライアントがキーを送る必要はありません。
ブラウザーで信頼できないページを開く
ブラウザーがHeadroomのWebSocketエンドポイントに到達できると、ページのスクリプトは接続を試みられます。ブラウザーはハンドシェイクにページのOriginを含めます。
脆弱なハンドラーがOriginポリシーなしで接続を受け入れる
影響を受けるハンドラーでは、信頼できないOriginを拒否せずwebsocket.accept()に到達します。同一オリジンポリシーはサーバー側のWebSocket認可ではありません。
プロキシが認証済み要求を中継
Headroomは不足した上流Authorizationヘッダーを環境変数で補えます。攻撃者が管理するページはプロキシの権限で要求を発生させられます。影響は設定ツール、資格情報、ネットワーク到達性、ユーザー操作に左右されます。
修正はルーティング前に許可されないOriginを拒否
上流パッチはセッションを受け入れて中継する前にOriginを検証します。ブラウザーは既定でloopbackに制限され、明示的なOriginを設定でき、拒否した接続はコード1008で閉じられます。
ソースコード比較:脆弱 vs 堅牢化
アドバイザリはhandle_openai_responses_ws()を特定し、websocket.accept()前に検査がない点を説明しています。上流コミットc632023は_is_allowed_websocket_origin()を加え、許可されないOriginをルーティング前に拒否します。Originのないネイティブクライアントは維持され、ブラウザーOriginは既定でloopbackに限定され、HEADROOM_WS_ORIGINSまたはHEADROOM_CORS_ORIGINSで明示設定できます。この短い例は変更された信頼境界を示し、ヘルパー全体は省略しています。上流の回帰テストはOriginなし、loopback、リモート、不正形式、明示設定を検証します。
ソース抜粋: headroom/proxy/handlers/openai.py
async def handle_openai_responses_ws(self, websocket: WebSocket) -> None:
# 脆弱な形式: オリジン検証の前にブラウザーセッションを受け入れる。
await websocket.accept()
await self._relay_responses_session(websocket)
async def handle_openai_responses_ws(self, websocket: WebSocket) -> None:
ws_headers = dict(websocket.headers)
if not _is_allowed_websocket_origin(ws_headers):
logger.warning(
"event=websocket_origin_not_allowed origin=%r",
_header_get(ws_headers, "origin"),
)
await websocket.close(code=1008, reason="origin not allowed")
return
# ポリシーで許可されたブラウザーセッションだけが上流ルーティングに進む。
await websocket.accept()
await self._relay_responses_session(websocket)
エンジニアリング&システム堅牢化チェックリスト
- ✓Headroomを0.35.0以降へ更新し、依存ファイルだけでなくインストール済みパッケージとデプロイイメージの版を確認します。
- ✓
accept()や上流への転送より前にWebSocketのOriginを検証し、明示許可のない非loopback Originを拒否します。 - ✓Originのないネイティブクライアントを許可するのは、別の仕組みでクライアント種別を信頼できる場合に限ります。ブラウザーOriginの欠落は認可ではありません。
- ✓資格情報を持つプロキシの許可Originに
*を使わず、信頼するOriginを列挙して既定ポートの正規化をテストします。 - ✓可能なら開発用ローカルプロキシをloopbackにバインドし、他ホストから到達できる場合は認証またはネットワーク制御を必須にします。
- ✓可能な場合、権限と有効期間を限定した上流キーを使います。ブラウザー境界を越える要求にはshellなど高影響のツールを公開しません。
- ✓リモートOriginが上流ルーティング前にポリシー違反で閉じられるテストを追加し、loopback、設定Origin、許可済みネイティブクライアントの期待動作も確認します。
- ✓コマンド実行は設定済みツールの機能に依存します。Originの欠陥そのものを自動的なホストRCEと表現しません。