utsuroi csrf labs 知識ベース 02 / 02 — CSRF の仕組みと防御
CSRF Mechanism & Defense · Knowledge Base
02

CSRF の仕組みと防御

Three conditions for CSRF, and how to break each

CSRF が成立するには3つの条件が揃う必要があります。逆に言えば、その1つを崩すだけで攻撃を防げる。Token、SameSite、Referer 検証という3層の防御を理解しましょう。

1. CSRF が成立する3つの条件 #three-conditions

この3つすべてが揃わないと攻撃は失敗する。
  1. 状態を変更する処理がある — メール変更、パスワード変更、送金、投稿削除など
  2. Cookie だけで認証している — リクエストに含まれる情報が Cookie だけで完結
  3. リクエストパラメータが予測可能 — 攻撃者が必要なパラメータを全て事前に把握できる

一見当たり前に見えますが、3 が重要。例えば、メール変更フォームに「現在のパスワードを再入力」が必須なら、攻撃者はそのパスワードを知らないので CSRF は成立しない。CSRF Token も同じく「攻撃者には予測不可能な値」を必須にすることで攻撃を防ぐ。

2. 典型的な攻撃フロー #attack-flow

被害者が罠サイトを開いたら、もう手遅れ。
  1. 被害者がうつろい EC にログインしてマイページを開く(Cookie が保存される)
  2. 被害者が別のタブで攻撃者の罠サイト(beauty-news.evil)を開く
  3. 罠サイトには <form> + 自動 submit() が仕込まれている
  4. 被害者のブラウザは罠サイトの指示に従い、shop.utsuroi.example.com へ POST
  5. その POST には うつろい EC のセッション Cookie が自動添付されている
  6. サーバは Cookie を見て「ログイン済みのユーザだ」と判断、メールアドレスを変更
  7. 被害者は何が起きたか気づかない

被害者はクリック1回(罠サイトを開く)でなりすまし操作が完了します。罠サイトの URL を踏ませる方法はフィッシングメール、SNS、URL 短縮サービス、悪意ある広告など。

3. CSRF Token による防御 #token-defense

最も標準的で効果的な防御。

CSRF Token は、サーバが各セッションごとに生成する予測不可能なランダム値。フォームの hidden field として埋め込み、サーバ側で送信時に検証する。

// サーバがレスポンスとして返すフォーム
<form action="/account/email" method="POST">
  <input type="hidden" name="csrf_token" value="a8x9k2m7p3q4r5s6">
  <input type="email" name="email">
  <button type="submit">変更</button>
</form>

// サーバ側の検証ロジック
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
    abort(403);
}

攻撃者は被害者のセッションごとに生成された Token を知る術がない(Same-Origin Policy で攻撃者の JavaScript からは正規ページのレスポンスを読めない)。なので Token 検証で 403 になり攻撃失敗。

フレームワークのデフォルト機能

モダンな Web フレームワーク(Django、Rails、Laravel、Spring Security)は CSRF Token を標準機能として提供しています。アプリ実装者が明示的に無効化しない限り、自動的に保護されます。「自前で実装する必要がある」場合は、フレームワークの機能を確認するのが先。

4. SameSite Cookie による防御 #samesite-defense

ブラウザレベルで cross-site Cookie 送信を制限する。

詳細は 前章 §5 SameSite 参照。サーバが Set-Cookie: session=...; SameSite=Lax または SameSite=Strict を返すと、ブラウザが cross-site のフォーム POST 等で Cookie を送らないようになります。

Chrome 80(2020年)以降は SameSite 未指定時のデフォルトが Lax になったため、明示的に SameSite=None を指定しない限り、古典的 CSRF は自動的に防がれます。

ただし注意点:

5. Referer 検証による防御 #referer-defense

「どのページから飛んできたか」を確認する。

HTTP の Referer ヘッダ(綴りミスで歴史的にこのまま)は、リクエスト元のページの URL を示します。サーバ側で「Referer が自サイト以外なら拒否」とすれば、cross-site からのリクエストを弾けます。

// サーバ側の検証
$referer = $_SERVER['HTTP_REFERER'] ?? '';
if (!str_starts_with($referer, 'https://shop.utsuroi.example.com/')) {
    abort(403);
}
Referer 検証の落とし穴

Referer はブラウザが付ける情報で、プライバシー設定で送信されない場合がある(空または欠落)。「Referer が空ならどうするか」の判断が肝心:
・空でも許可 → 攻撃者がプライバシー機能を悪用して回避可能
・空なら拒否 → 正規ユーザの一部が使えなくなる
また、サブドメイン判定や URL の部分一致のミス(shop.utsuroi.example.com.evil.example を許してしまう等)も典型的な実装ミス。Referer 検証は補助的対策で、Token を主とすべきです。

6. Double Submit Cookie パターン #double-submit

Token をサーバ側で保存しないシンプル変種。

通常の CSRF Token はサーバ側でセッションに保存しますが、Double Submit Cookie パターンでは Token を Cookie とフォーム両方で送信し、サーバはそれが一致するかを検証します。

  1. サーバが Set-Cookie: csrf_token=a8x9k2...; HttpOnly=false を発行(JS から読めるようにする)
  2. クライアント JS が Cookie から token を読み、フォームの hidden field にセット
  3. 送信時、Cookie とフォームの値の一致をサーバが確認

攻撃者の罠サイトは Cookie を読めない(Same-Origin Policy)ので、フォームに正しい値を入れられず攻撃失敗。サーバ側でセッションストレージに Token を保存する必要がない利点があります。

SPA や API ファーストの設計でよく使われるパターン。SameSite と組み合わせて使うとさらに堅牢。