🔐 他のセキュリティ系記事もよかったらどうぞ:
CSRFの仕組みと防御を読み解く:CSRFトークンは銀の弾丸ではない
✅ この記事で触れること
- CSRF(クロスサイト・リクエスト・フォージェリ)の攻撃原理と実例
- ブラウザ仕様としてのCookie自動送信の仕組み
- CSRF トークン方式の仕組みと実装例(Laravel)
- SameSite 属性(Strict / Lax / None)の違いと選択基準
- SPA + Cognito など最近の構成での CSRF 対策の実情
- JWT の保存場所による CSRF 耐性・XSS 耐性の違い
- UX とセキュリティのバランスがなぜ難しいか
❌ この記事で触れないこと
- OAuth 2.0 や OIDC そのものの認可フロー詳細
- 脆弱性スキャナや WAF による CSRF 検出ルールの運用
- JWT の署名検証・暗号方式・失効処理といった低レイヤー実装
- SSRF / Clickjacking / Session Fixation 等の他の認証系攻撃
- WebAuthn や Passkey など次世代認証技術
CSRFとは
認証済みのユーザーが、攻撃者の用意したリクエストを意図せず Cookie 付きで送信させられてしまう攻撃。
CSRF の影響を受けるのは、認証や認可に基づいて動作し、主に POST 等のリクエストを受け付けるサイト。
☠ 攻撃のシナリオ ☠
ユーザーの行動例:
- インターネットバンキングにログインし、口座の残高を確認していた(特にログアウト操作はせず、そのままにしておいた)
- Facebook をうろうろしていたら、可愛い猫写真のサイトが気になってクリック!
- ☠ 猫 写真館(
cat-evil.com
)は罠サイト ☠ - 仕込まれた JS により、ブラウザに保存されていたインターネットバンキングのセッションクッキーを添えて不正リクエストが送信される
でも "銀行" と "罠サイト" はドメイン違うから安全では?
F12 で見える Cookie は「同一ドメインのものだけ」だったはず 🤔
これはブラウザの Same-Origin Policy(同一生成元ポリシー) による厳重な制限です。
でも… 「送信」はされてしまうのがポイント!
<form action="https://bank.example.com/send" method="POST">
<input type="hidden" name="amount" value="100000">
</form>
<script>document.forms[0].submit();</script>
このようなHTMLで、bank.example.com
の Cookie が自動で付与されてしまう。
なぜそんな仕様? ブラウザの不具合では
ブラウザが勝手に Cookie を送るのは 「便利さ」と「安全性のバランス」のため。
「送信先のドメインに対応するクッキーがあれば必ず送る」のが原則です。
今開いているページのドメインとは関係ありません!
もし全ての Cookie 付きリクエストをブロックしてしまうと…
- シングルサインオン(SSO)
- ログイン中のページ遷移
- 複数タブでのログイン維持
など、一般的な Web の利便性が失われてしまうため。
じゃあどうやって防ぐ?
IPA は「意図されたリクエストかどうかを確認する仕組みの導入」を推奨しています。
実装例として広く使われているのが、CSRFトークン方式です。
これは正規のフォームにしか埋め込めないランダム値を使って、「本物のリクエストであること」をサーバー側で検証する方法です。
具体的には…
正規サイトのフォーム内に CSRFトークン を配置し、リクエスト送信時に一緒に送信させます。
- サーバー側でセッションストアのトークンと送信されたトークンを比較
- 一致した場合のみリクエスト受付
- 不一致の場合は
403 Forbidden
多くのフレームワークでの実装例(Laravelの場合)
多くのWebフレームワーク(Django, Rails, Laravelなど)では、CSRFトークンによる保護が標準で備わっており、POSTなどの状態変更リクエストにはトークンが必須になります。
ここではその一例として、Laravel の実装方法を紹介します。
対策:
@csrf
これで以下の input が自動生成:
<input type="hidden" name="_token" value="ランダムな文字列">
検証:
ミドルウェア VerifyCsrfToken
が POST リクエスト時に、セッション内トークンと送信トークンを比較
✅ ただし... CSRFトークンは銀の弾丸ではない!
CSRF 対策の目的は「本人しか送れない仕組み」を作ること。
でも、「本人が送ってるつもりなのに失敗する」ような UX になってしまうと、それはそれで破綻しています。
😵💫 CSRFトークンでありがちな「UX 崩壊」パターン
- 長文入力して送信 → トークン期限切れで 403
- 複数タブで同じ画面を開いて操作 → 片方がトークン競合で失敗
- 自動保存つきSPA → トークン更新されず保存失敗(しかも静かに)
- ページ遷移せず fetch() だけ → 古いトークンのまま送信
→ ユーザー「なんでエラーになったの?」と困惑するやつです。
だからこそ、「ちょうどいいセキュリティ設計」 を探すのがエンジニアの仕事 ✨
SameSite Cookie の補助的効果
クッキーを発行する際に「別サイトからのアクセスには絶対このクッキー送らない」といった指示を設定できます
res.cookie('session', 'abc123', {
httpOnly: true,
sameSite: 'Lax',
});
属性の違い
属性 | 説明 | 主な用途 |
---|---|---|
Strict | 全クロスドメインでCookie送信拒否 | 金融機関など安全性最優先サイト |
Lax | クロスドメインのGETは送信OK | 通常のSPA構成 |
None | すべて許可(HTTPS必須) | サブドメイン/連携用途 |
SameSite Cookie の限界と問題点
- 古いブラウザで未対応
- クロスサイト動作(サードパーティ iframe・認証プロバイダ等)では
SameSite=None
必須
CSRF対策の多くは「ブラウザが正しく動作すること」を前提としています。
特に SameSite 属性などは、古いバージョンのブラウザではそもそも無視されることもあるため、ユーザー環境に依存した脆弱性も生まれがちです。
セキュリティ対応の観点からも、ブラウザは常に最新に保つ のがおすすめです。
AWS Cognito Hosted UI構成 + Next.js とかの SPA だと?
SameSite=Lax が現実的
- 同一ドメインでログイン後画面遷移も同一オリジン
- PKCEフローでトークン取得 → API通信
→ Lax
がバランス良 ◎
SameSite=Strict だと?
- ブックマークやメールリンク再訪時にクッキーが送られず、ログイン状態が維持されない
SameSite=None を使うべきケースは?
- サブドメインでAPIとフロントを分けている場合
- 認証プロバイダとの連携などクロスドメイン必須時
→ SameSite=None; Secure
が必要
※ SameSite=None にすると、Secure(HTTPS)属性が必須!(ブラウザ仕様で強制)
JWT なら CSRF は安心?
- 一般的に JWT の保存場所は
localStorage
やsessionStorage
が使用される - リクエストに、明示的に
Authorization: Bearer
を付ける必要あり
→ 自動送信されないので CSRF 耐性は強い
ただし:XSS に弱い(JS で読み取れる)
JWT でも HttpOnly Cookie に保存すれば CSRF の影響を受けます
トークンの送信方法(ヘッダかCookieか)で耐性が決まります。
総まとめ
-
CSRF は「Cookie を盗む」のではなく「使わせる」攻撃
-
対策は:
-
Cookie 制御(SameSite)
→ ブラウザに任せて抑制できるが、制限が強すぎると利便性が損なわれる
→ 古いブラウザでは意図通りに機能しないことも -
操作の検証(CSRFトークン)
→ 本人の操作かどうかをサーバー側で判定できるが、実装ミスやトークンの有効期限管理でUXを損ないやすい -
認証設計全体の見直し
→ JWT や SPA 構成を含めた、アプリケーション全体の認証・認可の仕組みと保存場所の整理が重要!
-
Cookie 制御(SameSite)
🐍 小さな蛇足
JWT 認証なのに Cookie が使われる? → Cognito はログイン UX 改善のため、セッションクッキーを併用しています。
OAuth2/OIDC は「どう保存するか」までは決めていません → 保存方式は実装に委ねられている。
ここまで読んでいただき、ありがとうございました!