
JupyterHub のクロスオリジン form POST による XSRF バイパス(Sec-Fetch-Mode: no-cors)— CWE-352
Sec-Fetch-Mode: no-cors)重大度: 中程度
CWE: CWE-352 — クロスサイトリクエストフォージェリ (XSRF)
影響を受けるバージョン: jupyterhub 4.1.0 ≤ version < 5.4.5 (patched in 5.4.5)
アドバイザリ: GHSA-m68r-v472-jgq9
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-40864
クレジット: Romain Deperne
JupyterHubのXSRF保護(4.1.0で再設計)は、リクエストが同一オリジンかどうかを判断するために Sec-Fetch-Mode リクエストヘッダーを使用していました。この保護は Sec-Fetch-Mode: no-cors を同一オリジンとして扱っていましたが、no-cors はブラウザが クロスオリジンの「シンプル」なフォーム送信 に対して送信するまさにその値です。その結果、Hubのフォームエンドポイント(/hub/spawn、/hub/accept-share)へのクロスオリジンのHTMLフォームPOSTは、XSRFチェックを完全にバイパスしていました。
JSON APIは影響を受けません(非シンプルなコンテンツタイプが必要であり、CORSプリフライトが強制されるため)。この方法で到達できるのはHTMLフォームエンドポイントのみです。
XSRFロジックは、同一オリジンのナビゲーションを捉えることを意図した一連の Sec-Fetch-* 状態に対して「信頼済み」と短絡判定します。この信頼セットには Sec-Fetch-Mode: no-cors が含まれていました。しかし、no-cors はブラウザが異なるオリジンへのプレーンな <form method=POST> に割り当てるモードであり、トークンが防ぐはずのまさに古典的なCSRFベクトルです。したがって、シンプルなフォーム本文を受け入れ、このXSRFゲートのみに依存している状態変更エンドポイントは、クロスオリジンで偽造可能です。
これを実際のエンドポイントに当てはめると、/hub/spawn(被害者のサーバーを起動)と /hub/accept-share(被害者に攻撃者のサーバーの共有を受け入れさせる)はどちらもこのゲートの背後にあるフォームPOSTです。
/hub/spawn — 攻撃者のページは、同意なしに被害者のシングルユーザーサーバーを起動できます
(リソース消費 / 予期しない状態の発生。攻撃者はそのサーバーへのアクセス権を得るわけではありません)。/hub/accept-share — 攻撃者が自身のサーバーを共有することを許可されているJupyterHubユーザーである場合、被害者に 共有を受け入れ させ、被害者が攻撃者のサーバーへアクセスできるように強制できます(さらなるソーシャルエンジニアリング / データ流出シナリオへの準備段階)。Sec-Fetch-Mode をオリジンの判定基準として使用するのは不健全です。no-cors は同一オリジンを意味しません。5.4.5 での修正は、no-cors を同一オリジンとして信頼しないようにします。直ちにアップグレードできない運用者は、リバースプロキシで Sec-Fetch-Mode: no-cors を含むリクエストを破棄できます。
poc/csrf_spawn.html — 任意の攻撃者オリジンでホストし、ログイン済みのJupyterHubユーザーにそれを開かせます。自動送信フォームは /hub/spawn へのクロスオリジンPOSTを発行します(ブラウザは Sec-Fetch-Mode: no-cors を送信)。脆弱なビルドは有効な _xsrf トークンなしでこれを受け入れ、被害者のサーバーを起動します。共有受け入れの亜種では、action を /hub/accept-share に指定します。
1. Edit TARGET-JUPYTERHUB in poc/csrf_spawn.html
2. Serve the file from an attacker origin (any static host)
3. Open it in a browser already authenticated to the target Hub
4. Observe the victim's server spawn with no XSRF token supplied
責任を持って開示しました。PoC は修正版のリリース後に公開されています。