CookieセッションではCSRFを考える
Cookieは条件に合うリクエストへ自動送信されます。これは便利ですが、CSRFの前提にもなります。
Cookieセッションでは、SameSiteだけ、CSRF tokenだけ、Origin検証だけに寄せず、構成に応じて組み合わせます。
CSRFの本質:
被害者はログイン済み
外部サイトが被害者のブラウザにリクエストを送らせる
Cookieが自動で付く
サーバーが本人の操作だと誤認する
まずGETで状態変更しない
最初の原則は、GETで状態変更しないことです。
避けたい例:
GET /account/delete
GET /logout
GET /settings/email?value=...
GETはリンク、画像、プリフェッチ、クローラーなどで意図せず呼ばれる可能性があります。
状態変更はPOST、PUT、PATCH、DELETEなどで扱います。
POST /logout
POST /settings/email
DELETE /account
これはCSRF対策の前提です。
SameSiteの位置づけ
SameSite=Lax は、多くの通常セッションで有効な防御になります。
Set-Cookie: session_id=...; HttpOnly; Secure; SameSite=Lax
Laxでは、多くのクロスサイトPOSTでCookieが送られにくくなります。通常のフォームCSRFに対して効果があります。
ただし、SameSiteだけに依存しない方がよい場面があります。
- 古いブラウザ対応
- OAuth/OIDC callback
- サブドメインをまたぐ構成
SameSite=Noneが必要なSSO- GETで副作用がある古いAPI
- 高リスク操作
SameSiteは強力ですが、アプリケーションの意図を知っているわけではありません。
CSRF token
CSRF token は、正規画面から発行されたリクエストかを確認するためのランダム値です。
フォーム例:
<form method="post" action="/settings/email">
<input type="hidden" name="csrf_token" value="random_token">
<input name="email">
<button>変更</button>
</form>
サーバー側:
sessionに保存したcsrf_tokenと
フォームから送られたcsrf_tokenが一致するか確認
重要なのは、攻撃者が外部サイトからそのtokenを知れないことです。
Synchronizer Token Pattern
サーバー側sessionにCSRF tokenを保存する方式です。
session:abc123 = {
userId: "user_001",
csrfToken: "random"
}
POST時:
Cookieのsession_idでsessionを読む
フォームのcsrf_tokenを読む
session.csrfTokenと一致すれば通す
Cookieセッションと相性がよく、初学者にも理解しやすい方式です。
Double Submit Cookie
Double Submit Cookie は、CSRF tokenをCookieとリクエスト本文・ヘッダーの両方で送る方式です。
Cookie: csrf_token=random
X-CSRF-Token: random
サーバーは両者が一致するか確認します。
ただし、単純なDouble SubmitはCookie injectionなどの注意点があります。署名付きtokenやsessionと紐づいたtokenにするなど、実装の安全性を確認します。
Origin / Referer 検証
状態変更リクエストでは、Origin ヘッダーを確認する方法もあります。
Origin: https://app.example.com
サーバー側で期待するOriginかを見ます。
Origin が https://app.example.com なら許可
それ以外なら拒否
Origin がない場合の扱い、プロキシ、古いブラウザ、同一オリジンフォームなどを考慮します。多くのWebアプリでは、CSRF tokenとOrigin検証を組み合わせると堅くなります。
SPAとAPIの場合
SPAでCookie認証のAPIを呼ぶ場合、CSRF対策は必要です。
例:
await fetch("/api/settings/email", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken
},
body: JSON.stringify({ email })
});
このとき、CSRF tokenをどこから取得するかを決めます。
候補:
- 初期HTMLに埋め込む
/api/csrf-tokenで取得する- HttpOnlyではないCSRF専用Cookieを読む
認証CookieはHttpOnlyにし、CSRF tokenは別扱いにします。CSRF tokenは認証情報そのものではなく、リクエストが正規UIから出ていることを確認するための値です。
ログアウトもPOSTにする
ログアウトは状態変更です。
POST /logout
GETログアウトは、外部リンクやプリフェッチで意図せず発火する可能性があります。
ログアウトは被害が小さく見えますが、ユーザー体験を壊し、ログイン状態を変える操作です。CSRF方針に合わせてPOSTで扱うのが自然です。
高リスク操作は再認証も使う
CSRF tokenがあれば何でも安全、とは考えません。
高リスク操作では、再認証やMFAを組み合わせます。
- パスワード変更
- メール変更
- 支払い方法変更
- 退会
- APIキー作成
- 管理者権限操作
CSRFは「意図しないリクエスト」を防ぐ対策です。本人端末が乗っ取られている、XSSがある、セッションが盗まれている場合は別の対策が必要になります。
実装判断の目安
| 構成 | 推奨 |
|---|---|
| 通常の同一siteフォーム | SameSite=Lax + CSRF token |
| 管理画面 | CSRF token + Origin検証 + 再認証 |
| SPA + Cookie API | credentials + CSRF header + Origin検証 |
| SSO / 外部連携あり | SameSite条件を個別確認 |
| GET副作用が残る旧API | まず設計を修正 |
CSRF対策は、アプリの構成で変わります。テンプレートを丸写しせず、Cookieがいつ送られるか、サーバーが何を本人操作とみなすかを確認します。
まとめ
Cookieセッションでは、CSRFを前提に設計します。
- GETで状態変更しない
SameSite=Laxを基本に検討する- CSRF tokenで正規画面からの送信を確認する
- Origin / Referer検証も組み合わせる
- SPAではCSRF headerを設計する
- ログアウトもPOSTにする
- 高リスク操作は再認証を検討する
SameSite、CSRF token、Origin検証、再認証を役割ごとに分けると、実装判断が安定します。