CSRF攻撃を実際に体験できるデモ環境を作ってみた
きっかけ
普段の開発で CSRF トークンの設定をしたり、なんとなく「CSRF ってクロスサイトからの不正なリクエストを防ぐやつでしょ」くらいの理解はあったのですが...
ちゃんと理解しようと思い、徳丸先生の動画を見てみました。
この動画がとても分かりやすく、CSRF 攻撃の流れや仕組みは理解できました。
でも、ふと思ったんです。
「実際に攻撃されるときってどうなるの?」
「どういった状況だと攻撃が成功するの?」
動画で仕組みは分かったけど、実際に手を動かして体験してみないと本当の理解には繋がらないなと。
というわけで、CSRF 攻撃を安全に体験できるデモ環境を作ってみました。
作ったもの
7 つのシナリオで CSRF 攻撃と防御を体験できる Docker Compose 環境です。
各シナリオには「銀行アプリ(攻撃対象)」と「攻撃者サイト」があり、実際に攻撃を試すことで「なるほど、こうやって攻撃されるのか」「この防御があると防げるのか」を体感できます。
用意したシナリオ
| シナリオ | ポート | 概要 | 分類 |
|---|---|---|---|
| 1. CSRF トークンなし | 8443/9443 | 最も基本的な脆弱性 | 攻撃 |
| 2. XSS + CSRF | 8444/9444 | XSS があればトークンも無意味 | 攻撃 |
| 3. GET ベース CSRF | 8445/9445 | GET で状態変更する危険性 | 攻撃 |
| 4. SameSite Cookie | 8446/9446 | None/Lax/Strict の違い | 比較 |
| 5. JSON API CSRF | 8447/9447 | 「JSON なら安全」の誤解を解く | 比較 |
| 6. Double Submit Cookie | 8448/9448 | トークンなしでの防御パターン | 防御 |
| 7. 完全ガード | 8449/9449 | 多層防御の実装例 | 防御 |
使い方
セットアップ
git clone https://github.com/tamoco-mocomoco/csrf-attack-sample.git
cd csrf-attack-sample
# SSL証明書を生成(初回のみ)
./generate-certs.sh
# 全デモを起動
docker-compose up -d --build
基本的な流れ
- 銀行アプリ(https://localhost:8443 等)でログイン
- 攻撃者サイト(https://localhost:9443 等)を開く
- 攻撃ボタンをクリック
- 銀行アプリに戻って残高を確認
ログイン情報: user1 / password123
実際に試してみる
🔓 シナリオ 1: CSRF トークンなし(攻撃)
まずは最も基本的な攻撃から。
- https://localhost:8443 にアクセスしてログイン
- 残高が ¥10,000 あることを確認
- https://localhost:9443 (攻撃者サイト)を開く
- 「攻撃実行」ボタンをクリック
- 銀行アプリに戻ると... 残高が減っている!
攻撃者サイトには、こんな隠しフォームが仕込まれています:
<form action="https://localhost:8443/transfer" method="POST">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="5000" />
</form>
<script>
document.forms[0].submit();
</script>
ユーザーが銀行にログイン済みなら、このフォームを送信するだけで送金が実行されてしまいます。ブラウザが自動的に Cookie を付けて送信してくれるからです。
💉 シナリオ 2: XSS + CSRF(攻撃)
CSRF トークンがあっても、XSS があれば無意味になることを体験します。
- https://localhost:8444 にアクセスしてログイン
- https://localhost:9444 (攻撃者サイト)で XSS ペイロードをコピー
- 銀行アプリのメッセージボードに貼り付けて投稿
- ページをリロード → 自動的に攻撃実行!
XSS があると、攻撃者は被害者のブラウザ上でスクリプトを実行できます。つまり:
- CSRF トークンを読み取れる
- 正規のリクエストと同じ形式で送信できる
🔗 シナリオ 3: GET ベース CSRF(攻撃)
GET で状態変更する設計がいかに危険かを体験します。
- https://localhost:8445 にアクセスしてログイン
- https://localhost:9445 を開く
- ページを開いた瞬間に攻撃完了!
攻撃者サイトには、こんなコードが仕込まれています:
<img src="https://localhost:8445/transfer?to=attacker&amount=5000" />
<img> タグの src 属性に URL を指定するだけで、GET リクエストが自動送信されます。ユーザーは何もクリックしていないのに、送金が実行されてしまいます。
🍪 シナリオ 4: SameSite Cookie(比較)
SameSite 属性の違いを体験します。
- https://localhost:8446 にアクセス
- SameSite 設定を「None」にしてログイン
- https://localhost:9446 で攻撃 → 成功
- SameSite を「Strict」に変更して再ログイン
- 再度攻撃 → 失敗!
同じ攻撃コードなのに、Cookie の設定一つで結果が変わります。
| SameSite | POST 攻撃 | GET 攻撃 |
|---|---|---|
| None | ✅ 成功 | ✅ 成功 |
| Lax | ❌ 失敗 | ✅ 成功 |
| Strict | ❌ 失敗 | ❌ 失敗 |
📡 シナリオ 5: JSON API CSRF(比較)
「うちは JSON API だから CSRF は関係ない」と思っていませんか?
- https://localhost:8447 にアクセス
- Content-Type 検証を「無効」にしてログイン
- https://localhost:9447 で「方法 1: text/plain」を実行 → 成功
実は fetch() で Content-Type: text/plain を指定すれば、プリフライトリクエストなしで JSON を送れます:
fetch("https://localhost:8447/api/transfer", {
method: "POST",
headers: { "Content-Type": "text/plain" },
credentials: "include",
body: JSON.stringify({ to: "attacker", amount: 5000 }),
});
Content-Type 検証を「有効」にしても、CORS の設定が緩いと正しい Content-Type でも攻撃が通ります。
🍪 シナリオ 6: Double Submit Cookie(防御)
CSRF トークンなしでも防御できるパターンを体験します。
- https://localhost:8448 にアクセスしてログイン
- https://localhost:9448 で攻撃を試みる
- 攻撃失敗!
Double Submit Cookie は、Cookie とリクエストボディの両方に同じトークンを含め、サーバー側で一致を確認します。
攻撃者は Same-Origin Policy により被害者の Cookie を読み取れないため、正しいトークンをリクエストボディに含めることができません。
🛡️ シナリオ 7: 完全ガード(防御)
多層防御の実装例を確認します。
- https://localhost:8449 にアクセスしてログイン
- https://localhost:9449 で様々な攻撃を試みる
- 全ての攻撃が防がれる
このデモでは以下の対策を実装しています:
- ✅ CSRF トークン検証
- ✅ SameSite=Strict Cookie
- ✅ Origin/Referer チェック
- ✅ セキュリティヘッダー(Helmet)
一つの対策だけでなく、複数の防御層を組み合わせることで、より堅牢なセキュリティを実現できます。
学んだこと
CSRF 攻撃が成立する条件
- ユーザーが正規サイトにログイン済み
- セッション Cookie が有効
- 攻撃者がリクエストを偽装できる
効果的な防御(推奨順)
必須対策
- CSRF トークン - リクエストごとにランダムなトークンを検証
-
SameSite Cookie -
SameSite=LaxまたはStrictを設定 - RESTful 設計 - GET は読み取り専用、状態変更は POST
追加対策
- Origin/Referer チェック
- カスタムヘッダー(JSON API)
- Content-Type 検証(JSON API)
- セキュリティヘッダー(CSP, X-Frame-Options)
よくある誤解
❌ 「JSON API は CSRF に安全」
→ Content-Type を適切にチェックしないと脆弱
❌ 「SameSite=Lax で完全に保護される」
→ GET ベースの攻撃には脆弱
❌ 「HTTPS なら安全」
→ HTTPS は通信を暗号化するだけで CSRF は防げない
まとめ
実際に攻撃を体験してみると、理屈で分かっていたことが「なるほど、こういうことか」と腹落ちしました。
特に印象的だったのは:
- SameSite Cookie の効果の大きさ - 設定一つで攻撃の成否が変わる
- 多層防御の重要性 - 一つの対策だけでは不十分な場合がある
- GET で状態変更してはいけない理由 - img タグ一つで攻撃される
セキュリティは「なんとなく設定する」のではなく、なぜその設定が必要なのかを理解することが大切ですね。
ぜひ手を動かして試してみてください!
Discussion