CSRF とは何か
CSRF(Cross-Site Request Forgery)は、ログイン中のユーザーを罠サイトに誘導し、本人が意図しないリクエストを正規サービスに送らせる攻撃です。日本語では「リクエスト強要」「セッションライディング」とも呼ばれます。
鍵となるのは 「ブラウザは Cookie を勝手に付けてくれる」 という挙動です。被害者が銀行 A にログイン中(Cookie 保持)に、攻撃者の罠ページから A 宛にリクエストが飛ぶと、ブラウザはお行儀よく Cookie を付けてしまい、A から見ると「正規ユーザーの操作」に見えてしまいます。
典型的な攻撃シナリオ
被害者は銀行 bank.example.com にログイン済み。攻撃者は罠サイト evil.example に次の HTML を仕込みます:
<form action="https://bank.example.com/transfer" method="POST">
<input name="to" value="attacker">
<input name="amount" value="1000000">
</form>
<script>document.forms[0].submit()</script>被害者がこの罠を踏むと、ブラウザは bank.example.com の Cookie を自動付与して送信。サーバ側は Cookie が一致するので「本人の振込指示」と判断 → 送金完了。
攻撃者は レスポンスを読めない(同一オリジンポリシーで防がれる)点が重要。CSRF はあくまで「副作用のあるリクエストを発火させる」攻撃で、データを読み出す XSS とは性質が違います。
XSS との違い
| 観点 | XSS | CSRF |
|---|---|---|
| 攻撃の場 | 正規サイトのドメイン上で JS を実行 | 罠サイトから正規サイトへリクエスト送信 |
| レスポンスは読める? | 読める(同一オリジン下) | 読めない(CORSで遮断) |
| 攻撃の本質 | スクリプト実行 | 意図しない副作用リクエスト |
| 主防御 | 出力時エスケープ + CSP | SameSite Cookie + CSRFトークン |
XSS が成立すると CSRF 対策(トークン等)も同オリジンから読み取られて無効化されるので、XSS の方が圧倒的に強い攻撃です。CSRF 対策は XSS が無いことを前提にした「最低限の備え」と捉えるのが現実的。
防御1: SameSite Cookie
現代の CSRF 防御の主役は SameSite 属性です。Cookie に付けることで「クロスサイト遷移時の自動付与」を制限します。
| 値 | 挙動 | 用途 |
|---|---|---|
Strict | クロスサイトでは一切送らない | 銀行など最高機密 |
Lax(既定値) | トップレベル GET 遷移時のみ送る | 多くの一般サービス |
None | 常に送る(要 Secure) | サードパーティ埋込必須の場合 |
Chrome は 2020 年から 既定値が Lax。これにより「外部サイトから POST で罠送信」という古典 CSRF はかなりの確率で自動防御されるようになりました。とはいえ、明示的に Lax や Strict を指定し、クロスオリジン埋込が必要な Cookie だけ None; Secure にするのが安全です。
⚠
SameSite=Noneを使う場合は必ずSecure(HTTPS 限定)を付ける。Cookie Parser で属性可視化すると、None単独だとブラウザに拒否される警告が出ます。
防御2: CSRF トークン(Synchronizer Token Pattern)
フォーム送信時に「サーバが発行したランダムなトークン」を hidden field で持たせ、サーバ側で照合する方式です。攻撃者の罠サイトはこのトークンを取得できないので攻撃が失敗します。
<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token" value="${session.csrf_token}">
<input name="to" ...>
<input name="amount" ...>
</form>実装パターン:
- セッション結合: トークンをセッションに紐付け(最も一般的)
- Double Submit Cookie: Cookie とリクエストボディに同じ値を入れて照合(ステートレスで実装しやすい)
- 署名トークン: HMAC でユーザーID等に署名し、サーバ状態を持たない
Django, Rails, Laravel, Spring 等の主要フレームワークはデフォルトでこの仕組みを内蔵しています。「自分で書かない」ことが最大の安全策。
防御3: Origin / Referer ヘッダ検証
重要な操作(POST/DELETE 等)を受ける時、Origin ヘッダを見て「自サイトから来たリクエストか」を確認するシンプルな対策です。
if (req.headers.origin !== "https://bank.example.com") {
return res.status(403).end()
}モダンブラウザは状態変更系リクエストに Origin を必ず付けるため、SameSite Cookie + Origin チェックの組み合わせは強力。CSRF トークンとほぼ同等の効果を、状態保持なしで得られます。
防御4: カスタムリクエストヘッダ + CORS
SPA + API の構成では、API リクエストに X-Requested-With: XMLHttpRequest のようなカスタムヘッダを必須にする手があります。クロスオリジンでカスタムヘッダ付き POST はプリフライト(OPTIONS)が必要で、CORS が許可しなければそもそも本リクエストが飛びません。
さらに API を Cookie 認証ではなく Bearer トークン(Authorization: Bearer ...)にすれば、ブラウザが自動付与しないので CSRF はそもそも成立しません。SPA や JWT 認証では事実上これがベース防御になっています。
防御してはいけないやり方
- POST にすれば安全 ❌: form 自動 submit で容易に POST できるので無意味
- Referer 必須にする ⚠: 古いブラウザ・プライバシー設定で Referer が落ちることがあり、過剰拒否しがち。Origin の方が信頼できる
- CAPTCHA だけで対策 ⚠: 重要操作ごとに毎回 CAPTCHA は UX 破壊。お金が絡む操作の最終確認に限定する
おわりに
CSRF は「ブラウザが律儀に Cookie を付ける」前提で成立する攻撃で、現代では SameSite Cookie + CSRF トークン or Origin チェック の二段構えが定番です。SPA + Bearer トークン構成なら原理的に成立しないので、API 設計の段階で経路を選ぶ手もあります。
本サイトの HTTP Cookie Parser では、SameSite 属性の値ごとに警告メッセージが出るので、自社サービスの Set-Cookie 文字列を貼り付けてチェックしてみてください。