はじめに
Webアプリケーションのフォームに、こんな隠しフィールドを見たことはないでしょうか?
<input type="hidden" name="csrf_token" value="x7k9m2p4q1w3...">
これがCSRFトークンです。脆弱性診断でも「CSRFトークンの有無」は基本的なチェック項目の一つです。
しかし、「なぜこれが必要なのか?」「Cookieで認証しているのに、なぜそれだけでは不十分なのか?」を正確に説明できる人は意外と少ないのではないでしょうか。
本記事では、CSRFトークンの基礎を**「なぜ必要なのか」** から順を追って解説します。
結論
CSRFトークンは「そのリクエストが正規のページから送信されたものか」を検証するための使い捨ての秘密の値。Cookieは「誰か」を証明するが、「どこから送ったか」は証明できないため、CSRFトークンが必要になる。
前提知識:CSRF(Cross-Site Request Forgery)とは
CSRFは、ログイン中のユーザーのブラウザを悪用して、ユーザーが意図しない操作をサーバーに実行させる攻撃です。
なぜ成立するのか
ブラウザには「リクエスト送信時に、そのドメインのCookieを自動的に付与する」という仕様があります。これは正規サイトの利便性のための仕組みですが、攻撃者はこの仕様を逆手に取ります。
【正常な操作】
ユーザー → target.com の送金フォーム → target.com のサーバー
Cookie: session=abc123 が自動で付く ✅
【CSRF攻撃】
ユーザー → evil.com の罠ページ → target.com のサーバー
Cookie: session=abc123 が自動で付く 😈
→ サーバーは正規ユーザーからのリクエストと区別できない
ポイント:サーバーから見ると、どちらもCookie付きの正規リクエストに見える。
Cookieだけでは守れない理由
ここがCSRFトークンを理解するうえで最も重要な部分です。
Cookieが証明するもの・しないもの
| 項目 | Cookieで証明できるか |
|---|---|
| 「誰が」送ったか(認証状態) | ✅ できる |
| 「どこから」送ったか(送信元ページ) | ❌ できない |
Cookieはブラウザがリクエスト先のドメインに紐づけて自動送信する仕組みです。つまり、リクエストが target.com の正規ページから送られたのか、evil.com の罠ページから送られたのかを、Cookie自体は証明できません。
たとえ話:社員証だけでは防げない
社員証(Cookie)= 「この人は社員である」ことの証明
しかし…
・正面玄関から入ったのか?(正規ページから送信)
・裏口から侵入したのか?(罠ページから送信)
→ 社員証だけではわからない
CSRFトークン = 「正面玄関で発行される入館パス」
→ このパスを持っていれば正規ルートから来た証明になる
CSRFトークンの仕組み
基本的な流れ
【STEP 1】ページ表示時
サーバー → ランダムな値(CSRFトークン)を生成
サーバー → セッションにトークンを保存
サーバー → HTMLのhiddenフィールドにトークンを埋め込んで返す
【STEP 2】フォーム送信時
ブラウザ → Cookie(セッションID)+ CSRFトークンを送信
【STEP 3】サーバーでの検証
サーバー → セッション内のトークンとリクエスト内のトークンを比較
→ 一致すれば正規リクエスト ✅
→ 不一致または未送信なら拒否 ❌
なぜ攻撃者はトークンを用意できないのか
ここがCSRFトークンが有効な理由の核心です。
攻撃者が用意できるもの:
✅ target.com 宛てのリクエストを送る罠ページ
✅ ブラウザが自動送信するCookie(被害者の認証情報)
攻撃者が用意できないもの:
❌ CSRFトークンの値
なぜ用意できないのか?
- CSRFトークンはサーバーが毎回ランダムに生成する使い捨ての値
- トークンはHTMLのhiddenフィールドに埋め込まれている
- 攻撃者がtarget.comのHTMLを取得しようとしても、同一オリジンポリシーによりJavaScriptからは読み取れない
- 結果として、攻撃者は正しいトークン値を知ることができず、不正リクエストは検証で弾かれる
CSRFトークンが埋め込まれた正規フォームの例
<!-- target.com の送金ページ -->
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="x7k9m2p4q1w3">
<input type="text" name="to" placeholder="送金先">
<input type="number" name="amount" placeholder="金額">
<button type="submit">送金する</button>
</form>
POST /transfer HTTP/1.1
Host: target.com
Cookie: session=abc123
Content-Type: application/x-www-form-urlencoded
csrf_token=x7k9m2p4q1w3&to=yamada&amount=10000
サーバーはセッションに保存したトークンと、リクエスト内の csrf_token パラメータを照合します。一致すれば処理を実行、不一致なら拒否します。
攻撃者の罠ページにはトークンがない
<!-- evil.com の罠ページ -->
<form action="https://target.com/transfer" method="POST">
<!-- csrf_token がない、または間違った値 -->
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="1000000">
</form>
<script>document.forms[0].submit();</script>
POST /transfer HTTP/1.1
Host: target.com
Cookie: session=abc123 ← Cookieは自動送信される
Content-Type: application/x-www-form-urlencoded
to=attacker&amount=1000000
← csrf_token がない!→ サーバーが拒否 ✅
CSRFトークンの要件
有効なCSRFトークンには以下の要件が求められます。
| 要件 | 理由 |
|---|---|
| 推測不可能であること | 暗号論的に安全な乱数で生成する。連番や予測可能な値はNG |
| ユーザーごとに異なること | 他のユーザーのトークンが使い回せてはならない |
| 使い捨て(またはセッション単位)であること | 古いトークンが再利用されるリスクを防ぐ |
| サーバー側で検証されること | クライアント側のみのチェックでは意味がない |
CSRFトークン以外の対策との比較
CSRFトークンだけが対策ではありません。他の手法との比較を整理します。
| 対策 | 仕組み | 強度 | 備考 |
|---|---|---|---|
| CSRFトークン | 正規ページで発行されたトークンを検証 | 高 | 最も一般的で確実な対策 |
| SameSite Cookie | 外部サイトからのリクエストにCookieを付与しない | 高 |
Lax はGET遷移を許容、Strict は完全遮断 |
| Referer検証 | リクエスト元URLを確認 | 中 | Refererが送信されない環境もある |
| CORSの設定 | クロスオリジンリクエストを制限 | 中 | formのPOST送信はCORSの対象外のため単独では不十分 |
補足:SameSite Cookieとの関係
SameSite=Lax や Strict を設定すれば、外部サイトからのリクエストにCookieが送信されなくなるため、CSRF自体が成立しにくくなります。しかし、CSRFトークンとSameSite Cookieは併用するのがベストプラクティスです。 多層防御の考え方で、一方が突破されても他方で防御できます。
まとめ
| ポイント | 内容 |
|---|---|
| CSRFとは | ログイン中のユーザーの権限で意図しない操作を実行させる攻撃 |
| Cookieだけでは不十分 | 「誰か」は証明できるが「どこから」は証明できない |
| CSRFトークンとは | リクエストが正規ページから送信されたことを証明する使い捨ての秘密の値 |
| なぜ有効か | 攻撃者は同一オリジンポリシーにより正規のトークン値を知ることができない |
| ベストプラクティス | CSRFトークン + SameSite Cookie の併用(多層防御) |
参考資料
この記事は脆弱性診断エンジニアとしての学習メモを兼ねています。
誤りや補足があればコメントいただけると嬉しいです。