CSRFの成立条件と多層防御の原理
勝手に送金・設定変更されるCSRFの正体を、なぜブラウザが他サイト発のリクエストにCookieを付けて送るのかから解明。各防御の効きどころを見極め、堅い設計の判断軸が手に入ります。
- 1.CSRF はブラウザが「リクエストの宛先ドメインのCookieを自動付与する」性質を悪用する攻撃。攻撃者は中身を読めず、認証済みの副作用だけを起こす。
- 2.成立には3条件(Cookieベースの認証/推測可能なリクエスト/攻撃者がレスポンスを読む必要がない)が揃うことが必要。1つでも崩せば防げる。
- 3.防御は SameSite Cookie・Origin/Referer検証・トークン同期/二重送信Cookie の多層で組む。効きどころと前提が各々違い、単独では穴が残る。
CSRF(Cross-Site Request Forgery)は「攻撃者のサイトから、被害者の認証情報を使って、別サイトへ副作用のあるリクエストを飛ばす」攻撃です。鍵は、攻撃者がレスポンスを読む必要が一切ないこと。送金や設定変更が「実行されさえすれば勝ち」という構造が、CORSやSame-Origin Policyでは防ぎきれない理由になっています。
なぜ他サイト発のリクエストにCookieが付くのか
ブラウザのCookie送信ルールは「宛先(リクエスト先)のドメインに紐づくCookieを自動付与する」というものです。リクエストの発信元がどこかは、従来は問われませんでした。つまり攻撃者のページ evil.example に置かれたフォームが bank.example へ POST すると、ブラウザは律儀に bank.example のセッションCookieを添えて送ります。
この自動付与こそがCSRFの土台です。攻撃者はCookieの値を知る必要がありません。読めなくても、ブラウザが勝手に正しいCookieを付けてくれるからです。
<!-- evil.example に置かれた罠。被害者が開くだけで送金が走る -->
<form action="https://bank.example/transfer" method="POST" id="f">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="100000">
</form>
<script>document.getElementById('f').submit();</script>
SOP / CORS が制限するのは「レスポンスの中身を JS に読ませるか」です。一方 CSRF は副作用(状態変更)だけが目的で、レスポンスを読みません。フォーム送信や画像読み込みによるリクエストの送出そのものは、これらの仕組みでは止まらないのがポイントです。読ませない防御と、送らせない防御は別物だと切り分けてください。
成立の3条件
CSRF が刺さるには、次の3つが同時に成り立つ必要があります。逆に言えば、どれか1つでも崩せば成立しません。
| 条件 | 内容 | 崩すと… |
|---|---|---|
| Cookieベース認証 | 認証状態がCookieで保持され自動付与される | Authorizationヘッダ等にすれば自動付与されない |
| リクエストの推測可能性 | 攻撃者がパラメータを事前に組み立てられる | 予測不能なトークンを必須にすると組み立て不能 |
| レスポンス不要 | 副作用が起きれば目的達成(読む必要なし) | —(攻撃の性質なので崩せない) |
3つ目は攻撃の本質なので除去できません。だから現実の防御は、上の2つ(自動付与される認証情報/推測可能なリクエスト)のいずれかを成立させないことに集約されます。
トークン同期(Synchronizer Token)
最も古典的かつ堅い防御です。サーバーがセッションごとにランダムなトークンを生成・保持し、フォームの hidden フィールドに埋め込みます。送信時にサーバーは「セッションに紐づくトークン」と「リクエストに付いてきたトークン」を比較し、一致しなければ拒否します。
効く理由は、攻撃者がトークン値を知り得ないからです。トークンはレスポンスHTMLの中にあり、攻撃者は被害者のレスポンスを読めません(SOPが効く)。よって正しい hidden 値を埋めたフォームを攻撃者は構築できず、条件②(推測可能性)が崩れます。
1. GET /form → サーバーが token=Q7x... を生成しsessionに保存、HTMLに埋込
2. POST /transfer (token=Q7x... を含む)
3. サーバー: session.token == body.token か検証 → 不一致なら 403
同期トークンは「サーバーがトークンを保持して照合する」点が肝です。セッションストアに状態を持つため、ステートレスにしたい構成では次の二重送信Cookieが選ばれます。SPA では meta タグや初回レスポンスでトークンを配り、以降のリクエストにカスタムヘッダで載せる形が一般的です。
二重送信Cookie(Double Submit Cookie)
サーバーに状態を持たせない方式です。ランダムなトークンを Cookie とリクエスト本体(hidden もしくはカスタムヘッダ)の両方に入れ、サーバーは「2つの値が一致するか」だけを見ます。セッションストアへの照合が不要なのでステートレスに組めます。
効く理屈は、攻撃者は Cookie の値を読めない(SOPで遮断)うえ、別ドメインから bank.example の Cookie を任意の値に書き換えることもできないから、Cookie 側とボディ側を一致させられない、というものです。
二重送信Cookie は「Cookie を攻撃者が制御できない」前提に依存します。しかしサブドメインの脆弱性などで攻撃者が example ドメインに Cookie を**注入(cookie tossing)**できると、自分が知る値を Cookie とボディの双方に揃えられ、防御が破れます。対策として、トークンをセッションに署名付きで束縛する(HMAC でセッション識別子と結合する)署名付き二重送信が推奨されます。単なる「2値が一致」では不十分です。
Origin / Referer ヘッダ検証
ブラウザが付ける Origin(および Referer)はリクエストの発信元を示します。サーバー側で「発信元が自分のオリジンか」を確認すれば、evil.example 発の POST を弾けます。条件②ではなく「発信元の素性」を直接見る防御です。
Origin は CSRF に効きやすいヘッダです。クロスサイトの POST には付与され、JS から偽装できません(fetch でも Origin はブラウザが上書きするユーザー制御不可ヘッダ)。
| 観点 | Origin | Referer |
|---|---|---|
| 含む情報 | scheme + host + port のみ | フルURL(パス・クエリ含む) |
| 欠落の起こりやすさ | 比較的安定して付く | プライバシー設定等で省略され得る |
| 検証方針 | 許可オリジンと完全一致を要求 | 存在時のみ host を検証(無ければ別防御に委ねる) |
Origin/Referer 検証の落とし穴は、ヘッダが欠落したときの扱いです。「無ければ素通し」にすると、ヘッダを落とせる経路で回避されます。原則は fail-closed(判断材料が無ければ拒否)。ただし正規の同一オリジン GET 等で欠落するケースもあるため、状態変更リクエスト(POST/PUT/DELETE 等)に限って厳格化し、トークン方式と併用するのが堅実です。
SameSite Cookie
そもそも条件①(クロスサイトでCookieが自動付与される)をブラウザ側で断つのが SameSite 属性です。Cookie 自体に「別サイト発のリクエストには付けるな」と指示します。
| 値 | クロスサイトでの送信 | 効きどころ・注意 |
|---|---|---|
| Strict | 一切送らない | 外部リンクから戻った直後も未ログイン扱いになりUXに影響 |
| Lax | トップレベルGETナビゲーションのみ送る | 多くのブラウザの既定。フォームPOSTには付かずCSRFを大幅に抑止 |
| None | 常に送る(Secure必須) | クロスサイト用途で明示。CSRF保護は別途必要 |
既定が Lax になったことで、フォーム POST のようなクロスサイトの副作用リクエストには Cookie が付かなくなり、典型的なCSRFの多くが自動的に無力化されました。ただし Lax は「トップレベル GET ナビゲーション」には Cookie を送るため、状態変更を GET で実装していると穴になります。状態変更は必ず POST 等にすること。
SameSite=Lax は強力ですが万能ではありません。(1)「サイト」は eTLD+1 単位なので、同一サイト内のサブドメインからの攻撃(サブドメイン乗っ取り等)は防げません。(2) クライアントが古い、または属性が正しく付いていないと無効化されます。(3) None を選ばざるを得ない構成では保護が外れます。SameSite は土台として敷きつつ、トークンや Origin 検証を重ねる多層防御が前提です。
多層で組む
各防御は前提と効きどころが異なります。実務では複数を重ね、どれか1つが破れても全体が崩れないようにします。
| 防御 | 崩す条件 | 主な弱点・前提 |
|---|---|---|
| SameSite=Lax | ①Cookie自動付与 | サブドメイン経由/None指定/GETでの状態変更に弱い |
| Origin/Referer検証 | 発信元の素性 | ヘッダ欠落時の扱い/同一サイト内攻撃 |
| 同期トークン | ②推測可能性 | サーバー状態が必要/トークン配布設計 |
| 署名付き二重送信 | ②推測可能性 | ステートレス可だがcookie tossing対策に署名必須 |
推奨の重ね方は、(a) すべての認証 Cookie に SameSite=Lax(最低限)+ Secure + HttpOnly を付ける、(b) 状態変更は必ず非 GET にする、(c) その上で同期トークンまたは署名付き二重送信を必須化、(d) Origin 検証を fail-closed で追加、という構成です。
なお、JS から読み書きするトークン方式は XSS に弱い点に注意してください。XSS があればトークンは漏れ、CSRF 防御は丸ごと迂回されます。CSRF 対策と並行して、入力のサニタイズや Content Security Policyの内部動作と回避耐性 による XSS 抑止を組み合わせるのが本筋です。
土台となる認証情報の運び方は Cookie とセッション と Cookie・セッション・JWT(Web認証) を、なぜ「読む防御」と「送る防御」が別なのかは 同一オリジンポリシーとサイト分離の信頼境界 もあわせてどうぞ。
Web/フロントエンド Article
CSRFの成立条件と多層防御の原理を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
CSRF
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
成立には3条件(Cookieベースの認証/推測可能なリクエスト/攻撃者がレスポンスを読む必要がない)が揃うことが必要。1つでも崩せば防げる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「CSRF / セキュリティ」に近いか確認する。
- 強みである「CSRF はブラウザが「リクエストの宛先ドメインのCookieを自動付与する」性質を悪用する攻撃。攻撃者は中身を読めず、認証済みの副作用だけを起こす。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。