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 認証では事実上これがベース防御になっています。
防御方式の比較(どれを選ぶか)
これらは排他ではなく重ねて使うのが基本です。まず SameSite を土台に敷き、構成に応じてトークンや Origin 検証を足します。
| 方式 | サーバ状態 | SPA / API 向き | 備考 |
|---|---|---|---|
| SameSite Cookie | 不要 | △(Cookie 認証前提) | まず入れる土台 |
| Synchronizer Token | 必要(セッション) | △ | 最も確実・FW 内蔵 |
| Double Submit Cookie | 不要 | ○ | ステートレス・実装に注意 |
| Origin / Referer 検証 | 不要 | ○ | 軽量・SameSite と併用 |
| Bearer トークン(Cookie 不使用) | 不要 | ◎ | 原理的に CSRF が成立しない |
状態を変えるリクエストを GET にしない
副作用のある操作を GET で公開してはいけません。<img src> や <a> で自動的に発火でき、SameSite=Lax でもトップレベルの GET 遷移は Cookie が送られるため、削除・課金・設定変更を GET にすると CSRF の格好の的になります。状態変更は必ず POST / PUT / DELETE にし、トークンか Origin を検証します。
// 脆弱: GET で状態変更、検証なし(<img src> だけで発火する)
app.get('/account/delete', (req, res) => {
deleteAccount(req.session.userId);
});
// 修正: POST + Origin 検証 + CSRF トークン照合
app.post('/account/delete', (req, res) => {
if (req.headers.origin !== 'https://bank.example.com') return res.sendStatus(403);
if (req.body.csrf_token !== req.session.csrfToken) return res.sendStatus(403);
deleteAccount(req.session.userId);
});Login CSRF という変種
CSRF はログイン前にも起こります。攻撃者が自分のアカウントの資格情報で被害者を勝手にログインさせ、被害者の操作(検索履歴・アップロード・保存した支払い情報など)を攻撃者アカウントに紐付ける「Login CSRF」です。見落とされがちですが、ログインフォームにも CSRF トークンを付けることで防げます。
防御してはいけないやり方
- POST にすれば安全 ❌: form 自動 submit で容易に POST できるので無意味
- Referer 必須にする ⚠: 古いブラウザ・プライバシー設定で Referer が落ちることがあり、過剰拒否しがち。Origin の方が信頼できる
- CAPTCHA だけで対策 ⚠: 重要操作ごとに毎回 CAPTCHA は UX 破壊。お金が絡む操作の最終確認に限定する
チェックリスト
- ☐ 状態変更は
POST/PUT/DELETEのみ(GET に副作用を持たせない) - ☐ Cookie に
SameSite=LaxまたはStrict(クロスサイト埋込が要る物だけNone; Secure) - ☐ 重要操作で CSRF トークンまたは
Origin検証を実施 - ☐ ログインフォームにも CSRF トークン(Login CSRF 対策)
- ☐ Cookie に
Secure/HttpOnly - ☐ SPA / API は Bearer トークンかカスタムヘッダ必須 + CORS を厳格化
- ☐ XSS を塞いでいる(XSS があると CSRF 対策は無効化される)
関連ツールと記事
- HTTP Cookie Parser —
SameSite/Secure/HttpOnly属性を可視化し、危険な設定に警告 - Security Headers Analyzer — Cookie とあわせてセキュリティヘッダー全体を採点
- XSS の仕組みと対策 — CSRF 対策の大前提
- セッション認証と JWT の違い
- JWT のセキュリティ問題 — Bearer トークン構成での注意点
参考(一次情報)
おわりに
CSRF は「ブラウザが律儀に Cookie を付ける」前提で成立する攻撃で、現代では SameSite Cookie + CSRF トークン or Origin チェック の二段構えが定番です。SPA + Bearer トークン構成なら原理的に成立しないので、API 設計の段階で経路を選ぶ手もあります。状態変更を GET にしないという基本も忘れずに。
まずは自社サービスの Set-Cookie 文字列を HTTP Cookie Parser に貼り付け、SameSite 属性が意図どおりか確認してみてください。