はじめに

クロスサイトリクエストフォージェリ(Cross-Site Request Forgery:CSRF)は、ログイン中の利用者になりすまして、パスワード変更、メールアドレス変更、送金、設定変更などを実行させる攻撃です。
攻撃者がパスワードやセッションCookieを盗むとは限りません。
利用者のブラウザが、ログイン済みサイトへCookieを自動送信する性質を悪用します。
主要ブラウザのSameSite Cookieや、Djangoなどのフレームワーク標準防御によって、昔ながらの単純なCSRFは成功しにくくなりました。しかし、CSRFが消滅したわけではありません。
GETによる状態変更、不完全なトークン検証、SameSiteの誤設定、OAuthログイン、サブドメイン侵害、WebSocket、クライアントサイドCSRFなど、現代のWeb構成に合わせた攻撃面が残っています。
この記事では、CSRFの歴史、種類、成立条件、防御方式、Djangoでの実装、確認方法、再発防止まで順番に解説します。
結論・要点
CSRF対策の結論は、1つの仕組みに依存せず、複数の防御を重ねることです。
特に重要なのは、次の構成です。
状態を変更する処理をGETで実行しない
Djangoの
CsrfViewMiddlewareを有効にするHTMLフォームに
{% csrf_token %}を入れるJavaScriptから送信する場合は
X-CSRFTokenヘッダーを付けるセッションCookieとCSRF Cookieに
Secureと適切なSameSiteを設定するOriginおよびRefererの検証を無効化しないCSRF_TRUSTED_ORIGINSを必要最小限にするXSS、サブドメイン、CORS、OAuth、WebSocketも一緒に確認する
送金や認証情報変更では再認証やワンタイム確認を追加する
Djangoをセキュリティサポート中のバージョンへ更新する
DjangoではCSRF防御が標準で有効です。独自実装へ置き換えるより、標準機能を正しく使う方が安全です。
2026年7月29日時点では、Django 6.0系と5.2 LTS系にセキュリティ更新が提供されており、2026年7月7日にDjango 6.0.7と5.2.16が公開されています。Django 4.2は2026年4月に延長サポートが終了しました。CSRF対策だけでなく、今後の脆弱性修正を受け取るためにも、4.2系を使っている環境は5.2 LTS以降への移行が必要です。
この記事で分かること
この記事では、次の内容を理解できます。
CSRFで何が起きるのか
CSRFとXSS、CORS、クリックジャッキングの違い
CSRFの歴史と現代までの変化
フォーム型、ログインCSRF、API型などの攻撃分類
CSRFトークンが防御になる理由
Synchronizer TokenとDouble Submit Cookieの違い
SameSite Cookieだけでは不十分な理由
DjangoのCSRF防御の内部構造
Djangoテンプレート、Fetch、Axios、DRFでの実装
403エラーの確認方法
Nginx、Cloudflare、WAFでできる補助対策
テスト方法と再発防止策
対象読者・前提環境
対象読者は次のとおりです。
DjangoでWebアプリケーションを開発している人
Cookie認証を利用している人
REST APIやSPAを開発している人
CSRFとXSSの違いを整理したい人
セキュリティ試験やWeb診断を学習している人
自社サービスのCSRF設定を点検したい運用担当者
主な前提環境は次のとおりです。
Python 3.12以降
Django 5.2 LTSまたはDjango 6.0系
HTTPS
Nginxなどのリバースプロキシ
Cookieベースのセッション認証
DjangoテンプレートまたはJavaScriptフロントエンド
1. CSRFとは
1-1. ブラウザに「本人のふり」をさせる攻撃
CSRFは、宅配便の受取人確認に例えられます。
配達員が「この家から出された依頼書だから本人の依頼だろう」と判断し、依頼書を書いた人物を確認しなければ、第三者が偽の依頼書を送り込めます。
Webでも同じことが起こります。
サーバーがセッションCookieだけを確認している場合、次の2つを区別できないことがあります。
利用者が正規画面から送信したリクエスト
悪意ある別サイトが利用者のブラウザに送信させたリクエスト
CSRFは、利用者のブラウザを経由して後者を実行させます。
正式には、CSRFは「利用者が認証済みであるというサーバーとの信頼関係を悪用し、利用者が意図していないリクエストを送信させる攻撃」です。OWASPやMDNも、認証済みブラウザに望まない操作を実行させる攻撃として説明しています。
1-2. CSRFの基本的な流れ
① 利用者が正規サイトへログイン
victim.example
│
│ セッションCookieを取得
▼
利用者のブラウザ
② 利用者が攻撃者サイトを開く
evil.example
│
│ 自動送信フォーム
▼
利用者のブラウザ
③ ブラウザが正規サイトへリクエスト
Cookie: session=利用者のセッション
│
▼
victim.example
④ 正規サイトが本人の操作と誤認
メール変更・送金・設定変更などを実行
重要なのは、攻撃者がセッションCookieの中身を知らなくてもよい点です。
ブラウザが送信先に対応するCookieを自動的に付けるため、攻撃者はブラウザにリクエストを送らせるだけで攻撃できる場合があります。
1-3. CSRFが成立する条件
典型的なCSRFは、次の条件が重なった場合に成立します。
| 条件 | 内容 |
|---|---|
| 自動送信される認証情報 | セッションCookie、Basic認証、クライアント証明書など |
| 予測可能な処理 | URL、パラメータ、送信形式が攻撃者に分かる |
| 意図確認がない | CSRFトークン、Origin検証、再認証がない |
| 利用者がログイン中 | 有効な認証状態がブラウザに残っている |
| 外部サイトから送信可能 | HTMLフォーム、画像、リンクなどでリクエストを作れる |
| 状態変更が可能 | メール変更、送金、削除、権限変更などが実行される |
Cookieを使わず、JavaScriptがメモリ上のアクセストークンを明示的にAuthorizationヘッダーへ付ける構成では、古典的CSRFは成立しにくくなります。
ただし、トークンをCookieへ保存して自動送信している場合や、CORSを誤設定している場合は別です。
2. CSRFの歴史
2-1. 1990年代後半から2000年代
初期のWebアプリケーションでは、認証Cookieがあれば本人の操作と判断する実装が一般的でした。
当時は次のような設計も珍しくありませんでした。
GET /account/delete?id=123
GET /admin/add-user?name=attacker
GET /transfer?to=attacker&amount=100000
攻撃者は、画像タグやリンクを利用してGETリクエストを送信させられました。
<img src="https://example.com/account/delete?id=123">
この時代は、状態変更をGETで実行する設計と、送信元を検証しない設計が大きな原因でした。
CSRFは「Session Riding」「One-Click Attack」などの呼び方でも知られるようになりました。
2-2. OWASP Top 10での扱い
CSRFは、OWASP Top 10の過去版で独立したリスクとして扱われていました。
2013年版では「A8 Cross-Site Request Forgery」として掲載されていましたが、2017年版では独立項目から外れました。
OWASPは2017年版のリリースノートで、主要フレームワークがCSRF防御を備えるようになり、調査対象アプリケーションでの検出率が約5%だったことを理由として説明しています。
ただし、独立項目から外れたことは「CSRFがなくなった」という意味ではありません。
フレームワークの標準機能を無効化したり、独自APIやSPAで防御を実装し忘れたりすれば、現在でも成立します。
2-3. CSRFトークンの普及
2000年代後半から2010年代にかけて、主要フレームワークがCSRFトークンを標準機能として実装しました。
代表例は次のとおりです。
Django
Ruby on Rails
Spring Security
ASP.NET
Laravel
Symfony
正規サイトが発行した秘密の値をリクエストに含め、Cookieだけでなくトークンも一致した場合に処理を許可します。
攻撃者サイトはSame-Origin Policyにより正規サイトの画面やレスポンスを自由に読み取れないため、正しいトークンを取得できません。
2-4. SameSite Cookieの普及
その後、CookieにSameSite属性が普及しました。
SameSiteは、別サイトを起点とするリクエストへCookieを付けるかどうかを制御します。
主要な設定は次の3種類です。
| 設定 | クロスサイト送信 | 特徴 |
|---|---|---|
Strict | 原則送信しない | 最も強いが外部リンクからの遷移で不便が出る |
Lax | 一部のトップレベルGETで送信 | 利便性と安全性のバランス |
None | 送信する | Secureが必須。CSRF対策を別途行う |
ChromeではSameSite未指定CookieにLax相当を適用する仕組みが導入され、単純なクロスサイトPOST型CSRFは成功しにくくなりました。
しかし、SameSiteは「同一オリジン」ではなく「同一サイト」を基準にします。
例えば、次の2つは異なるオリジンですが、同一サイトとして扱われる可能性があります。
https://app.example.com
https://upload.example.com
upload.example.comにXSSやオープンリダイレクトなどの脆弱性があると、app.example.comのSameSite防御を迂回する足場になる場合があります。
2-5. 現代のCSRF
現代では、単純なHTMLフォーム型だけでなく、次のような境界領域が重要です。
OAuth・OpenID ConnectのログインCSRF
SPAのクライアントサイドCSRF
Cross-Site WebSocket Hijacking
GraphQL MutationへのCSRF
JSON APIのContent-Type処理不備
サブドメインを利用したSame-Site攻撃
メソッドオーバーライドによる検証回避
新しく発行されたCookieのSameSite例外
CORS誤設定との組み合わせ
サービスワーカーやブラウザ拡張機能を含む複雑な送信経路
CSRFは消滅したのではなく、フレームワーク標準機能で防げる単純な問題から、認証・ブラウザ・API設計を横断する問題へ変化しています。
3. CSRF攻撃の種類
3-1. GET型CSRF

GETリクエストで状態変更が行われる場合に成立します。
<img src="https://example.com/account/delete">
または次のようなリンクでも成立します。
<a href="https://example.com/admin/promote?user=attacker">
画像を見る
</a>
原因
GETでデータを変更している
CSRF検証がPOSTにしか適用されていない
メソッドオーバーライドを許可している
対策
GET、HEAD、OPTIONS、TRACEは状態を変更しない「安全なメソッド」として扱います。
DjangoのCSRFミドルウェアも安全なメソッドを検証対象外にします。そのため、GETで削除や更新を実装すると、DjangoのCSRF防御を通過してしまいます。
3-2. POSTフォーム型CSRF

攻撃者サイトに自動送信フォームを置く方式です。
<form action="https://example.com/account/email" method="post">
<input type="hidden" name="email" value="[email protected]">
</form>
<script>
document.forms[0].submit();
</script>
セッションCookieがクロスサイトPOSTに付与され、サーバーがCSRFトークンを確認しなければ、変更処理が実行されます。
3-3. ログインCSRF

利用者を攻撃者のアカウントへログインさせる攻撃です。
一見すると、攻撃者側のアカウントへ切り替わるだけに見えます。
しかし、利用者が気付かずに次の情報を登録すると問題になります。
クレジットカード
個人情報
閲覧履歴
検索履歴
機密ファイル
SNS投稿
クラウドデータ
情報は攻撃者のアカウントへ保存されるため、攻撃者が後から確認できます。
ログイン画面にもCSRF防御が必要です。
3-4. ログアウトCSRF

外部サイトからログアウトURLを呼び出し、利用者を強制的にログアウトさせます。
直接的な情報漏えいがなくても、次の問題につながります。
作業中データの消失
サービス妨害
偽ログイン画面への誘導
ログインCSRFとの組み合わせ
セッション状態の混乱
ログアウトもPOSTとCSRFトークンで実行する設計が望まれます。
3-5. JSON CSRF

「JSON APIだからCSRFは起こらない」とは限りません。
通常のHTMLフォームは、自由なapplication/jsonリクエストを送れません。そのため、JSONのみを厳格に受け付けるAPIは古典的フォームCSRFに比較的強くなります。
しかし、次の場合は危険です。
text/plainでもJSONとして解析するContent-Typeを確認しない
URLエンコード形式も受け付ける
GETパラメータをMutationとして処理する
CORSで任意OriginとCookie送信を許可する
サーバーがJSON以外へ寛容すぎる
メソッドオーバーライドを許可する
JSONという形式そのものが防御になるわけではありません。
3-6. API・SPA型CSRF

React、Vue、AngularなどのSPAでも、認証情報をCookieで管理する場合はCSRF対策が必要です。
SPA
│
│ Cookie: session=...
▼
Django API
JavaScriptから送るためHTMLフォームを使っていなくても、ブラウザがセッションCookieを自動送信する構成は変わりません。
対策として、CSRFトークンをカスタムヘッダーへ入れます。
X-CSRFToken: トークン
カスタムヘッダーを付けたクロスオリジンリクエストは通常CORSプリフライトの対象になります。
ただし、CORSを広く許可すると防御が弱くなるため、許可Originを限定する必要があります。
3-7. Client-Side CSRF

クライアントサイドCSRFは、JavaScriptコード自身が攻撃者の入力を使って、意図しないAPIリクエストを組み立てる問題です。
例えば、URLフラグメントやクエリ文字列から送信先を作るコードです。
const endpoint = location.hash.substring(1);
fetch(endpoint, {
method: "POST",
credentials: "include",
headers: {
"X-CSRFToken": getToken()
}
});
攻撃者が次のURLを踏ませたとします。
https://example.com/app#/api/account/delete
JavaScript自身が正規オリジン上で動いているため、CSRFトークンやCookieを正しく付けて危険なリクエストを送る可能性があります。
従来型CSRFでは「攻撃者サイトが直接送る」のに対し、クライアントサイドCSRFでは「正規サイトのJavaScriptを誤動作させる」という違いがあります。
対策
URLから任意のAPIパスを作らない
操作名と送信先を固定マッピングする
許可リストを使う
スキーム、ホスト、パスを検証する
認証情報変更では再確認を要求する
3-8. Cross-Site WebSocket Hijacking

WebSocketのハンドシェイクでもCookie認証を使う場合があります。
サーバーがOriginを検証しないと、攻撃者サイトから被害者のCookieを伴うWebSocket接続を開ける可能性があります。
evil.example
│
│ WebSocket handshake
│ Cookie: session=...
▼
wss://example.com/ws/chat/
これはCross-Site WebSocket Hijacking、略してCSWSHと呼ばれます。
PortSwiggerも、WebSocketハンドシェイクを対象にしたCSRFの一種として説明しています。
Django Channelsでの対策
OriginValidatorを使うAllowedHostsOriginValidatorを使うALLOWED_HOSTSを厳密にする接続後にもユーザー権限を確認する
workspace、room、tenantの所属確認を行う
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from channels.auth import AuthMiddlewareStack
application = ProtocolTypeRouter({
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(websocket_urlpatterns)
)
),
})
3-9. OAuth・OpenID ConnectのCSRF

OAuth認可フローでは、攻撃者が自分の認可レスポンスを被害者のブラウザへ注入し、アカウントの対応関係を入れ替える攻撃が問題になります。
主な防御は次のとおりです。
statenoncePKCE
redirect URIの完全一致
認可コードの一回限り利用
セッションとの関連付け
OpenID Connectライブラリの利用
stateは単なる固定文字列ではなく、予測困難でログイン開始時のセッションに結び付ける必要があります。
PKCEは認可コード横取りだけでなく、認可フローに対する一部のCSRFやコード注入対策にも関係します。
3-10. Same-Site・Cross-Origin型
SameSite Cookieにおける「site」と、Same-Origin Policyにおける「origin」は異なります。

https://app.example.com
https://blog.example.com
この2つは次の関係です。
Same-Site: Yes
Same-Origin: No
そのため、脆弱な兄弟サブドメインがあると、SameSite Cookieだけでは防げない場合があります。
特に注意すべき場所は次のとおりです。
ユーザーアップロード用サブドメイン
古い検証環境
廃止予定の管理画面
外部サービスへ委譲したサブドメイン
サブドメインテイクオーバーの可能性があるDNS
任意リダイレクトを持つサブドメイン
機密システムとユーザー生成コンテンツは、単なるサブドメイン分離ではなく、別の登録可能ドメインへ分ける方が強い分離になります。
4. CSRFと似た攻撃の違い
4-1. CSRFとXSS
| 比較 | CSRF | XSS |
|---|---|---|
| 主な目的 | 利用者に操作させる | 正規サイト上でスクリプトを動かす |
| レスポンスの読取り | 通常は困難 | 可能 |
| Cookieの値 | 知らなくても成立する | HttpOnlyでなければ盗める場合がある |
| 攻撃場所 | 外部サイトなど | 脆弱な正規サイト内 |
| 主な対策 | CSRFトークン、SameSite、Origin検証 | 出力エスケープ、CSP、サニタイズ |
XSSが存在すると、正規ページからCSRFトークンを読み取ったり、正規オリジンからリクエストを送ったりできます。
そのため、OWASPもXSSによってCSRF対策が破られ得ると注意しています。
CSRF対策を強化しても、XSSを放置してよいわけではありません。
4-2. CSRFとCORS
CORSは、別オリジンのJavaScriptがレスポンスを読み取れるかどうかを制御する仕組みです。
CSRFは、レスポンスを読めなくても攻撃できます。
そのため、CORSを無効にしただけではCSRF対策になりません。
また、次の組み合わせは危険です。
Access-Control-Allow-Origin: https://evil.example
Access-Control-Allow-Credentials: true
任意Originを動的反射し、Cookie送信を許可する設定も危険です。
4-3. CSRFとクリックジャッキング
クリックジャッキングは、透明なiframeなどを重ねて、利用者に見えていないボタンを押させる攻撃です。
CSRFは、利用者がボタンを押さなくても自動送信で成立する場合があります。
DjangoではXFrameOptionsMiddlewareやCSPのframe-ancestorsを使って、クリックジャッキングも別途防ぎます。
5. CSRFトークンはなぜ防御になるのか
5-1. 合鍵を追加する仕組み
セッションCookieだけを確認する状態は、住所だけで本人確認しているようなものです。
CSRFトークンは、正規画面を開いた利用者だけが取得できる合鍵です。
認証Cookie + CSRFトークン
│
▼
両方が正しい場合だけ状態変更を許可
攻撃者サイトはリクエストを送信できても、Same-Origin Policyによって正規サイトのレスポンスを自由に読めません。
そのため、秘密で予測困難なトークンを正しく取得できません。
5-2. 安全なCSRFトークンの条件
安全なトークンには次の性質が必要です。
十分に長い
暗号学的に予測困難
利用者のセッションに関連付いている
状態変更リクエストすべてで検証する
トークン欠落時も拒否する
GETパラメータへ入れない
URLやアクセスログへ残さない
外部送信フォームへ埋め込まない
XSSから保護する
ログイン時などに必要に応じて更新する
PortSwiggerは、トークンをセッションへ結び付け、すべての対象リクエストで厳密に検証することを重要条件として挙げています。
6. CSRFトークンの種類
6-1. Synchronizer Token Pattern
サーバー側セッションにトークンを保存する方式です。
サーバーセッション
┌──────────────────┐
│ session_id: ABC │
│ csrf: XYZ123 │
└──────────────────┘
フォーム
<input type="hidden" name="csrf" value="XYZ123">
リクエスト時に、フォームの値とサーバー側セッションの値を比較します。
長所
セッションへ確実に結び付けられる
他ユーザーのトークンを流用しにくい
実装モデルが分かりやすい
短所
サーバー側に状態を保存する
複数タブや戻る操作との調整が必要な場合がある
リクエストごとに使い捨てると操作性が悪化する場合がある
6-2. Double Submit Cookie
同じランダム値をCookieとリクエストパラメータまたはヘッダーに入れ、両方が一致するか確認します。
Cookie:
csrf=XYZ123
Header:
X-CSRF-Token: XYZ123
攻撃者サイトはCookieを送信させることはできても、そのCookie値を読み取ってカスタムヘッダーへコピーできない、という性質を利用します。
ただし、単純に値が一致するだけの方式は、サブドメインなどからCookieを設定できる場合に破られる可能性があります。
6-3. Signed Double Submit Cookie
Double Submit Cookieの値へHMACなどの署名を付け、認証セッションと結び付けます。
OWASPは、単純なDouble Submit Cookieよりも、セッションへ明示的に結び付けたSigned Double Submit Cookieを推奨しています。
概念的には次のような構造です。
token = random + HMAC(server_secret, session_id + random)
サーバーは次を確認します。
Cookieとヘッダーの値が一致する
HMAC署名が正しい
現在のセッションに結び付いている
独自実装は比較処理、エンコード、鍵管理、トークン固定などのミスが起きやすいため、フレームワーク標準機能を優先します。
6-4. Django方式
DjangoはCSRF Cookieにランダムな秘密値を保存します。
フォームに入るトークンは、BREACH攻撃を軽減するため、秘密値をそのまま出力せず、リクエストごとにランダムなマスクを加えた値です。
受信時にはマスクを解除し、Cookie内の秘密値と比較します。
さらに、Djangoは次の検証も行います。
GET、HEAD、OPTIONS、TRACE以外を検査
CSRF Cookieの存在確認
フォームまたはヘッダーのトークン確認
Originヘッダー検証HTTPSでOriginがない場合の厳格なReferer検証
CSRF_TRUSTED_ORIGINSとの照合
Django公式ドキュメントでは、ログイン時にCSRF秘密値を変更し、フォーム側トークンを毎回異なるマスクで出力する構造が説明されています。
7. 防御方式の評価
| 防御 | 強さ | 単独利用 | 注意点 |
|---|---|---|---|
| CSRFトークン | 高い | 推奨しない | XSSがあると破られる可能性 |
| SameSite Cookie | 中~高 | 不十分 | GET、Same-Site攻撃、例外挙動 |
| Origin検証 | 高い | 補助向け | 非ブラウザクライアントへの配慮 |
| Referer検証 | 中 | 単独は非推奨 | Referrer-Policyとの競合 |
| カスタムヘッダー | 高い | APIで有効 | CORS設定が重要 |
| 再認証 | 高い | 重要操作向け | UXへの影響 |
| CAPTCHA | 低~中 | 単独不可 | 自動送信を妨げるだけ |
| WAF | 補助 | 単独不可 | 正規形式の偽造リクエストを識別しにくい |
| Fetch Metadata | 中~高 | 補助向け | 非対応クライアントへの移行設計 |
| ユーザー操作確認 | 高い | 重要操作向け | 毎操作では負担が大きい |
8. SameSiteだけではCSRFを完全に防げない理由
8-1. LaxではトップレベルGETにCookieが付く
SameSite=Laxでは、利用者がリンクを開くようなトップレベルGETナビゲーションでCookieが送信される場合があります。
状態変更をGETで実行していれば、CSRFが成立します。
8-2. メソッドオーバーライド
アプリケーションやフレームワークが次のパラメータを受け付ける場合があります。
_method=POST
_method=DELETE
外側はGETでも、内部ではPOSTやDELETEとして処理される実装では、SameSiteの条件とアプリケーションのメソッド判定が食い違う可能性があります。
8-3. Same-Siteの兄弟ドメイン
同一登録可能ドメイン配下では、異なるサブドメインでもSame-Siteとなる場合があります。
脆弱な兄弟サブドメインを経由すると、SameSite防御を迂回できる可能性があります。
8-4. 新しく発行されたCookie
ChromeのLax-by-default実装では、SSOなどを壊さないため、新しく発行されたCookieについて一時的な例外挙動が問題になる場合があります。
明示的なSameSite=Laxと、未指定時にブラウザが適用するLax相当の挙動が完全に同じとは限りません。
Cookie属性はブラウザの暗黙設定に任せず、アプリケーション側で明示します。
9. Fetch Metadataによる追加防御
現代のブラウザは、リクエストの発生状況を示すFetch Metadataヘッダーを送る場合があります。
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
主な値は次のとおりです。
| ヘッダー | 内容 |
|---|---|
Sec-Fetch-Site | same-origin、same-site、cross-site、none |
Sec-Fetch-Mode | navigate、cors、no-cors、same-origin、websocketなど |
Sec-Fetch-Dest | document、image、iframe、emptyなど |
Sec-Fetch-User | 利用者操作によるナビゲーションか |
サーバーは、状態変更エンドポイントへSec-Fetch-Site: cross-siteで届いたリクエストを拒否できます。
W3Cの仕様では、Fetch Metadataはサーバーがリクエストの生成コンテキストを判断し、アプリケーション到達前に不審なリクエストを拒否するための情報として定義されています。
Djangoミドルウェア例
from django.http import HttpResponseForbidden
class FetchMetadataMiddleware:
SAFE_METHODS = {"GET", "HEAD", "OPTIONS"}
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
fetch_site = request.headers.get("Sec-Fetch-Site")
if (
request.method not in self.SAFE_METHODS
and fetch_site == "cross-site"
):
return HttpResponseForbidden(
"Cross-site state-changing request denied."
)
return self.get_response(request)
ただし、次の経路は事前に確認します。
ネイティブアプリ
古いブラウザ
APIクライアント
Webhook
外部IdP
決済サービス
OAuthコールバック
クロスサイト埋め込み
Fetch MetadataはCSRFトークンの代わりではなく、追加防御として使います。
10. DjangoでCSRFを守る基本設定
10-1. ミドルウェアを有効にする
settings.pyを確認します。
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
CsrfViewMiddlewareは標準プロジェクトで有効です。
削除しないでください。
Django公式も、CSRFミドルウェアを無効化することを推奨していません。
10-2. DjangoテンプレートのPOSTフォーム
<form method="post" action="{% url 'account_update' %}">
{% csrf_token %}
<label for="email">メールアドレス</label>
<input id="email" name="email" type="email" required>
<button type="submit">変更する</button>
</form>
{% csrf_token %}は<form>の中に置きます。
外部サイトへPOSTするフォームには入れません。外部へCSRFトークンが漏れるためです。
10-3. 関数ベースビュー
通常はミドルウェアで保護されます。
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseNotAllowed
from django.shortcuts import redirect, render
@login_required
def account_update(request):
if request.method == "GET":
return render(request, "account/update.html")
if request.method != "POST":
return HttpResponseNotAllowed(["GET", "POST"])
request.user.email = request.POST["email"]
request.user.save(update_fields=["email"])
return redirect("account_complete")
GETでは画面を表示するだけにします。
変更処理はPOSTで行います。
10-4. クラスベースビュー
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.views.generic import UpdateView
from .models import Profile
class ProfileUpdateView(LoginRequiredMixin, UpdateView):
model = Profile
fields = ["display_name"]
template_name = "profile/update.html"
success_url = reverse_lazy("profile_detail")
def get_queryset(self):
return Profile.objects.filter(user=self.request.user)
CSRF対策だけでなく、他人のオブジェクトを変更できないよう、クエリセットを本人へ限定します。
CSRFは認証された利用者の権限を使う攻撃です。認可不備があると被害が拡大します。
11. JavaScript Fetchでの実装
11-1. CSRF Cookieを取得する
function getCookie(name) {
const cookies = document.cookie ? document.cookie.split(";") : [];
for (const rawCookie of cookies) {
const cookie = rawCookie.trim();
if (cookie.startsWith(`${name}=`)) {
return decodeURIComponent(cookie.slice(name.length + 1));
}
}
return null;
}
11-2. ヘッダーへ付ける
async function updateProfile(payload) {
const csrfToken = getCookie("csrftoken");
if (!csrfToken) {
throw new Error("CSRFトークンを取得できませんでした。");
}
const response = await fetch("/api/profile/", {
method: "POST",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrfToken,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const message = await response.text();
throw new Error(
`更新に失敗しました: ${response.status} ${message}`
);
}
return response.json();
}
同一オリジンAPIでは、原則としてcredentials: "same-origin"を使います。
外部OriginとのCookie共有が本当に必要な場合だけincludeを検討します。
11-3. HttpOnly設定時の注意
CSRF_COOKIE_HTTPONLY = Trueにすると、JavaScriptからdocument.cookieでCSRF Cookieを読めません。
その場合は、HTML内のトークンを取得します。
<form id="csrf-form">
{% csrf_token %}
</form>
function getCsrfTokenFromDom() {
const input = document.querySelector(
"[name=csrfmiddlewaretoken]"
);
if (!input) {
throw new Error("CSRFトークン要素がありません。");
}
return input.value;
}
CSRF_COOKIE_HTTPONLYは、CSRF対策の本質的な強化ではありません。
XSSが存在すれば、攻撃者はCookieを直接読めなくてもDOM内のトークンを読んだり、正規JavaScriptとしてリクエストを送ったりできます。
12. 推奨するDjango設定
# settings.py
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = "Lax"
SESSION_COOKIE_SAMESITE = "Lax"
CSRF_COOKIE_HTTPONLY = False
SESSION_COOKIE_HTTPONLY = True
CSRF_TRUSTED_ORIGINS = [
"https://kurutann.com",
"https://www.kurutann.com",
]
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
設定の意味
| 設定 | 推奨 | 内容 |
|---|---|---|
CSRF_COOKIE_SECURE | True | HTTPSだけでCSRF Cookieを送る |
SESSION_COOKIE_SECURE | True | HTTPSだけでセッションCookieを送る |
CSRF_COOKIE_SAMESITE | LaxまたはStrict | クロスサイト送信を制限 |
SESSION_COOKIE_SAMESITE | LaxまたはStrict | セッションCookieを制限 |
SESSION_COOKIE_HTTPONLY | True | JavaScriptによるセッションCookie読取りを防ぐ |
CSRF_TRUSTED_ORIGINS | 最小限 | 信頼する送信元Origin |
SECURE_SSL_REDIRECT | True | HTTPSへ強制 |
| HSTS | 段階導入 | HTTPSダウングレードを防ぐ |
Django 6.0ではSESSION_COOKIE_SAMESITEのデフォルト値はLaxです。
HSTSの注意
SECURE_HSTS_INCLUDE_SUBDOMAINSとSECURE_HSTS_PRELOADは、すべてのサブドメインがHTTPS対応済みであることを確認してから設定します。
誤設定すると、HTTPしか使えないサブドメインへ接続できなくなる場合があります。
13. CSRF_TRUSTED_ORIGINSの正しい設定
Django 4系以降では、スキームを含むOriginを指定します。
CSRF_TRUSTED_ORIGINS = [
"https://example.com",
"https://www.example.com",
]
必要な場合だけワイルドカードを使います。
CSRF_TRUSTED_ORIGINS = [
"https://*.example.com",
]
しかし、すべてのサブドメインを信頼すると、古い検証環境やユーザーコンテンツ用サブドメインの脆弱性が本番へ影響する可能性があります。
原則として、完全なOriginを個別指定します。
次のような広すぎる設定は避けます。
# 推奨しない
CSRF_TRUSTED_ORIGINS = [
"https://*.example.com",
]
特に、次のサブドメインがある場合は危険です。
uploads.example.com
staging.example.com
old.example.com
user-content.example.com
14. Nginxリバースプロキシの確認
Djangoへ正しいHostとスキームを伝えます。
location / {
proxy_pass http://django_upstream;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Cloudflareなどの前段がある場合、Django側でプロキシを信頼する設定を慎重に行います。
SECURE_PROXY_SSL_HEADER = (
"HTTP_X_FORWARDED_PROTO",
"https",
)
この設定は、信頼できるリバースプロキシだけがX-Forwarded-Protoを上書きできる構成で使用します。
インターネットからDjangoへ直接到達できる状態で、任意クライアントが同ヘッダーを偽装できる構成は避けます。
15. Django REST Frameworkでの注意
15-1. SessionAuthentication
DRFのSessionAuthenticationでログイン済みCookieを使う場合、CSRF検証が必要です。
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
],
}
フロントエンドからX-CSRFTokenを送ります。
15-2. Bearer Token認証
AuthorizationヘッダーへBearer Tokenを明示的に付ける構成では、典型的なCookie型CSRFは成立しにくくなります。
Authorization: Bearer eyJ...
ただし、次の場合は安全とは限りません。
Bearer TokenをCookieへ保存している
CORSで不正Originを許可している
トークンをlocalStorageへ保存し、XSSがある
リフレッシュトークンをCookieで自動送信している
認証方式がエンドポイントごとに混在している
CSRFを避けるためにlocalStorageへ移した結果、XSSによるトークン窃取リスクが増えることがあります。
CookieとlocalStorageのどちらが常に安全という単純な話ではありません。
16. csrf_exemptを安易に使わない
次のデコレーターはCSRF検証を無効化します。
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def webhook(request):
...
外部Webhookでは必要になる場合があります。
しかし、無条件で受け入れてはいけません。
代わりに次を検証します。
HMAC署名
タイムスタンプ
リプレイ防止
イベントIDの重複
送信元サービスの公開鍵署名
Content-Type
許可HTTPメソッド
ボディサイズ
必要に応じた送信元IP
import hashlib
import hmac
from django.conf import settings
from django.http import (
HttpResponseBadRequest,
HttpResponseForbidden,
JsonResponse,
)
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
@csrf_exempt
@require_POST
def payment_webhook(request):
signature = request.headers.get("X-Webhook-Signature")
if not signature:
return HttpResponseBadRequest("Missing signature")
expected = hmac.new(
settings.WEBHOOK_SECRET.encode(),
request.body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected):
return HttpResponseForbidden("Invalid signature")
return JsonResponse({"status": "accepted"})
csrf_exemptは「認証不要」という意味ではありません。
CSRFとは別の強固な送信元検証へ置き換える必要があります。
17. CSRFが発生していないか確認する方法
17-1. Django設定を確認する
目的
CSRFミドルウェアとCookie設定が有効か確認します。
実行場所
Djangoコンテナまたは仮想環境内です。
python manage.py shell -c "
from django.conf import settings
print('CsrfViewMiddleware:', 'django.middleware.csrf.CsrfViewMiddleware' in settings.MIDDLEWARE)
print('CSRF_COOKIE_SECURE:', settings.CSRF_COOKIE_SECURE)
print('CSRF_COOKIE_SAMESITE:', settings.CSRF_COOKIE_SAMESITE)
print('SESSION_COOKIE_SECURE:', settings.SESSION_COOKIE_SECURE)
print('SESSION_COOKIE_SAMESITE:', settings.SESSION_COOKIE_SAMESITE)
print('CSRF_TRUSTED_ORIGINS:', settings.CSRF_TRUSTED_ORIGINS)
"
正常例
CsrfViewMiddleware: True
CSRF_COOKIE_SECURE: True
CSRF_COOKIE_SAMESITE: Lax
SESSION_COOKIE_SECURE: True
SESSION_COOKIE_SAMESITE: Lax
CSRF_TRUSTED_ORIGINS: ['https://example.com']
異常例
CsrfViewMiddleware: False
CSRF_COOKIE_SECURE: False
CSRF_TRUSTED_ORIGINS: ['https://*.example.com']
判断方法
ミドルウェアが
Falseなら重大な設定不備です本番HTTPS環境でSecureが
Falseなら修正します信頼Originが広すぎないか確認します
17-2. Djangoのデプロイ設定を検査する
目的
本番向けセキュリティ設定の不足を確認します。
実行場所
Djangoプロジェクトのルートです。
python manage.py check --deploy
正常例
System check identified no issues (0 silenced).
異常例
security.W008
security.W012
security.W016
判断方法
すべての警告がCSRFに直結するとは限りません。
ただし、HTTPS、Cookie、HSTS、DEBUG、秘密鍵などの警告は放置せず、一つずつ確認します。
17-3. Cookie属性を確認する
目的
実際のレスポンスにSecure、SameSiteなどが付いているか確認します。
実行場所
管理端末です。
curl -sS -D - -o /dev/null https://example.com/login/ \
| grep -i '^set-cookie:'
正常例
Set-Cookie: csrftoken=...; Path=/; SameSite=Lax; Secure
Set-Cookie: sessionid=...; HttpOnly; Path=/; SameSite=Lax; Secure
異常例
Set-Cookie: sessionid=...; Path=/
判断方法
本番HTTPS環境では、少なくとも次を確認します。
sessionidにSecuresessionidにHttpOnlysessionidにSameSitecsrftokenにSecurecsrftokenにSameSite
17-4. トークンなしPOSTが拒否されるか確認する
注意事項
本番の更新・削除エンドポイントへ直接試さないでください。
検証環境、専用テストユーザー、無害な確認用エンドポイントを使います。
目的
CSRFトークンなしの状態変更リクエストが403になるか確認します。
実行場所
管理端末です。
curl -i \
-X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'display_name=test' \
https://staging.example.com/profile/update/
正常例
HTTP/2 403
異常例
HTTP/2 200
HTTP/2 302
判断方法
200や更新完了画面への302が返る場合は、次を確認します。
csrf_exemptが付いていないかミドルウェアが無効でないか
GETでも更新できないか
独自APIが検証を迂回していないか
ログインが必要なエンドポイントでは、未認証のためログイン画面へ302になる場合があります。
正確な検証には、テストユーザーのセッションCookieを使い、CSRFトークンだけを除いた状態で確認します。
17-5. Djangoテストを書く
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from django.urls import reverse
class CsrfProtectionTests(TestCase):
def setUp(self):
self.client = Client(enforce_csrf_checks=True)
self.user = get_user_model().objects.create_user(
username="csrf-test-user",
password="test-password-123",
)
self.client.force_login(self.user)
def test_post_without_csrf_token_is_rejected(self):
response = self.client.post(
reverse("account_update"),
{"email": "[email protected]"},
)
self.assertEqual(response.status_code, 403)
def test_get_does_not_change_email(self):
original_email = self.user.email
response = self.client.get(
reverse("account_update"),
{"email": "[email protected]"},
)
self.user.refresh_from_db()
self.assertEqual(response.status_code, 200)
self.assertEqual(self.user.email, original_email)
通常のDjangoテストクライアントはCSRF検証を簡略化する場合があります。
CSRF自体を試験するときは、enforce_csrf_checks=Trueを指定します。
17-6. コードを検索する
目的
危険な無効化やGET更新処理を探します。
実行場所
Djangoプロジェクトのルートです。
grep -RIn \
--exclude-dir=.git \
--exclude-dir=.venv \
--exclude-dir=node_modules \
-E 'csrf_exempt|request\.GET|@require_GET|CSRF_TRUSTED_ORIGINS' \
.
正常例
payments/views.py:42:@csrf_exempt
Webhookなど、理由が説明できる少数箇所だけが見つかります。
異常例
accounts/views.py:18:@csrf_exempt
admin_api/views.py:51:@csrf_exempt
profile/views.py:77:request.GET.get("email")
判断方法
csrf_exemptが見つかったすべての箇所について、代替認証があるか確認します。
request.GETの利用自体は脆弱性ではありません。
GETパラメータを使って更新、削除、権限変更をしていないかを確認します。
18. DjangoでCSRF 403が発生したときの原因
代表的な原因は次のとおりです。
| 原因 | 確認箇所 |
|---|---|
{% csrf_token %}がない | HTMLフォーム |
| Cookieが送られていない | ブラウザ開発者ツール |
X-CSRFTokenがない | Fetch・Axios |
| Originが不一致 | リクエストヘッダー |
CSRF_TRUSTED_ORIGINS不足 | settings.py |
| HTTPとHTTPSが混在 | Nginx、Cloudflare |
| Hostが正しく伝わらない | proxy_set_header Host |
| X-Forwarded-Protoが不正 | Nginx、Django |
Referrer-Policyがno-referrer | レスポンスヘッダー |
| CSRF CookieがSecureなのにHTTP接続 | URL、TLS |
| ログイン後に古いトークンを送信 | SPA、キャッシュ |
| フォームが外部Originを向いている | action属性 |
DjangoはHTTPSでOriginがない場合、厳格なReferer検証を行います。
サイト全体へ次の設定を付けると、安全でないメソッドのフォーム送信が失敗することがあります。
Referrer-Policy: no-referrer
Django公式は、CSRFのReferer検証との競合を避けるため、外部リンク個別にrel="noreferrer"を使う方法を案内しています。
19. CSRF防御のよくある失敗
19-1. POSTだけトークンを確認する
PUT、PATCH、DELETEなども状態を変更します。
安全なメソッド以外はすべて検証対象にします。
19-2. トークンがある場合だけ検証する
次の実装は危険です。
if request.POST.get("csrf"):
validate_csrf(request.POST["csrf"])
トークンを削除すれば検証自体が実行されません。
正しくは、トークンがない場合も拒否します。
19-3. 全利用者で共通のトークンを使う
環境変数や固定文字列をそのままフォームへ入れる方式は、1人から漏れると全利用者へ悪用されます。
# 危険
CSRF_TOKEN = "fixed-secret-token"
トークンをセッションまたは認証状態へ関連付けます。
19-4. 短いトークンや連番を使う
csrf=12345
csrf=user_1001
csrf=20260729
予測可能な値は防御になりません。
暗号学的に安全な乱数を使います。
19-5. CSRFトークンをURLへ入れる
https://example.com/update?csrf=SECRET
URLは次の場所へ残る可能性があります。
ブラウザ履歴
アクセスログ
プロキシログ
解析サービス
Referer
スクリーンショット
監視ツール
トークンはフォームボディまたはカスタムヘッダーで送ります。
19-6. SameSiteだけに頼る
SameSiteは強力ですが、次には注意が必要です。
GETによる状態変更
Same-Siteのサブドメイン
クライアントサイドリダイレクト
ブラウザ差
SSO
WebView
Cookieの新規発行
SameSite=NoneCORS誤設定
CSRFトークンと併用します。
19-7. Refererだけに頼る
Refererはプライバシー設定、プロキシ、ブラウザポリシーなどで欠落することがあります。
欠落時に無条件で許可すると迂回されます。
Origin検証とCSRFトークンを中心にし、Refererは補完として使います。
19-8. WAFだけに頼る
CSRFリクエストは、形式上は正規利用者のリクエストに見えます。
正しいURL
正しいCookie
正しいパラメータ
正しいHTTP形式
そのため、シグネチャ型WAFだけで利用者の意図を判断することは困難です。
WAFは次の補助には使えます。
異常なOriginの遮断
Fetch Metadataによるポリシー
レート制限
ボット対策
不審な自動化の検知
既知の攻撃ペイロード検知
CSRFトークン検証はアプリケーション側で行います。
20. 重要操作に追加すべき防御
メールアドレス変更、パスワード変更、送金、APIキー発行、MFA解除などでは、CSRFトークンだけでなく追加確認を行います。
通常の状態変更
├─ セッション認証
├─ CSRFトークン
├─ Origin検証
└─ SameSite
重要な状態変更
├─ 上記すべて
├─ 現在のパスワード
├─ MFA
├─ ワンタイム確認
├─ 操作内容の再表示
└─ 監査ログ・通知
例
from django.contrib.auth import authenticate
from django.http import JsonResponse
from django.views.decorators.http import require_POST
@require_POST
def disable_mfa(request):
password = request.POST.get("password", "")
user = authenticate(
request,
username=request.user.get_username(),
password=password,
)
if user is None:
return JsonResponse(
{"error": "再認証に失敗しました。"},
status=403,
)
# MFA解除処理
return JsonResponse({"status": "ok"})
再認証はCSRF対策の代わりではありません。
CSRF防御を通過した場合の被害をさらに抑える多層防御です。
21. Cloudflare・WAF・IPS・EDRによる補助対策
Cloudflare・WAF
可能な補助対策は次のとおりです。
状態変更URLへの異常なクロスサイトOriginをログ化
Sec-Fetch-Site: cross-siteの監視管理URLのアクセス制限
Bot Management
レート制限
管理機能をAccessやZTNAで保護
不要なHTTPメソッドの拒否
ただし、OAuthコールバック、決済Webhook、外部連携を一律に遮断しないよう例外設計が必要です。
IPS
ネットワークIPSは、一般的なCSRFを正確に判定しにくい傾向があります。
通信形式が通常のPOSTと同じだからです。
一方、次の検知には役立ちます。
大量の設定変更試行
不自然なOrigin
異常なUser-Agent
短時間の大量リクエスト
既知の管理画面に対する自動攻撃
CSRFと組み合わせたXSSペイロード
EDR
CSRFは主にブラウザとWebアプリケーションの問題であり、EDR単独では防ぎにくい攻撃です。
ただし、CSRF後に不正ファイルのダウンロード、スクリプト実行、認証情報窃取などが発生した場合の検知に役立ちます。
22. 動作確認
22-1. 正規フォームの確認
正規画面を開く
csrftokenCookieを確認するHTML内に
csrfmiddlewaretokenがあることを確認する正常にPOSTできることを確認する
更新結果をDBで確認する
22-2. 不正リクエストの確認
次のケースが403になることを確認します。
CSRFトークンなし
CSRFトークンが空
1文字変更したトークン
別セッションのトークン
不正Origin
Cookieなし
安全でないメソッドへの不正リクエスト
22-3. GETで変更されないことを確認
次のURLへアクセスしても、データが変わらないことを確認します。
GET /account/delete/
GET /profile/update/[email protected]
GET /admin/promote/?user=attacker
GETでは確認画面を表示し、実際の変更はCSRF保護されたPOSTで行います。
22-4. ログの確認
DjangoではCSRF失敗がdjango.security.csrfロガーへ記録されます。
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"loggers": {
"django.security.csrf": {
"handlers": ["console"],
"level": "WARNING",
"propagate": False,
},
},
}
確認する項目は次のとおりです。
Origin checking failed
Referer checking failed
CSRF cookie not set
CSRF token missing
CSRF token incorrect
不自然な送信元IP
特定URLへの集中
デプロイ直後の急増
CSRF 403の急増は攻撃だけでなく、フロントエンド更新、CDN設定、Cookie属性、Origin設定の不整合でも発生します。
原因をログだけで断定しないようにします。
23. 再発防止
23-1. 開発標準を決める
次のルールを開発標準へ入れます。
・GETでは状態を変更しない
・Cookie認証のunsafe methodにはCSRF検証を必須とする
・csrf_exemptには理由と代替認証を記載する
・外部Webhookには署名検証を実装する
・CORSの許可Originを限定する
・管理機能には再認証を追加する
・兄弟サブドメインを同じ信頼境界とみなさない
23-2. CIで危険コードを検出する
#!/usr/bin/env bash
set -euo pipefail
if grep -RIn \
--exclude-dir=.git \
--exclude-dir=.venv \
--exclude-dir=node_modules \
'@csrf_exempt' .; then
echo "csrf_exemptが見つかりました。代替認証をレビューしてください。"
fi
python manage.py check --deploy
python manage.py test
csrf_exemptが存在するだけでビルドを必ず失敗させると、正当なWebhook実装も止まります。
検出結果をセキュリティレビュー対象にする運用が現実的です。
23-3. セキュリティテストを自動化する
最低限、次のテストを用意します。
トークンなしPOSTは403
誤ったトークンは403
正しいトークンは成功
GETでは変更されない
他人のリソースを更新できない
不正Originは拒否
ログイン画面もCSRF保護
ログアウトはPOST
OAuthのstate不一致は拒否
WebSocketの不正Originは拒否
23-4. Djangoを更新する
2026年7月29日時点では、Django 5.2 LTSまたは6.0系のセキュリティサポート中リリースを利用します。
バージョンを確認します。
python -m django --version
python -m pip show Django
依存関係を確認します。
python -m pip list --outdated
python -m pip check
注意事項
本番環境で直接アップグレードしないでください。
次の順番で進めます。
DBバックアップ
現在の依存関係を保存
検証ブランチ作成
テスト環境でアップグレード
Djangoリリースノート確認
自動テスト
CSRF・ログイン・管理画面テスト
ステージング確認
ロールバック手順作成
本番反映
24. 注意点・よくある誤解
誤解1:OWASP Top 10から消えたので対策不要
誤りです。
独立カテゴリから外れた理由の一つは、フレームワーク標準防御によって発生率が下がったためです。
標準防御を外せば、再び脆弱になります。
誤解2:SameSite=Laxなら完全に安全
誤りです。
GET更新、Same-Site攻撃、兄弟サブドメイン、クライアントサイドCSRFなどが残ります。
誤解3:POSTを使えば安全
誤りです。
外部サイトからHTMLフォームをPOSTできます。
トークン検証が必要です。
誤解4:JSON APIなら安全
条件付きです。
Content-Typeを厳格に確認し、Cookie認証ならCSRF対策を行い、CORSを正しく制限する必要があります。
誤解5:CORSを設定すればCSRFも防げる
誤りです。
CORSは主にレスポンスの読取りを制限します。
CSRFはレスポンスを読めなくても状態変更できる場合があります。
誤解6:CSRF CookieをHttpOnlyにすれば完璧
誤りです。
JavaScriptがトークンを読み取る構成では実装方法が変わります。
また、XSSがあればDOM内トークンの取得や正規オリジンからの送信が可能です。
誤解7:トークンはリクエストごとに必ず使い捨てるべき
必須ではありません。
リクエスト単位のワンタイムトークンは強力ですが、戻るボタン、複数タブ、再送信で問題が起きる場合があります。
セッション単位でも、十分な乱数、セッションへの関連付け、厳格な検証があれば有効です。
25. CSRF防御の将来
今後はCSRFトークンがなくなるというより、ブラウザとサーバーの複数シグナルを組み合わせる方向が進むと考えられます。
主な要素は次のとおりです。
SameSite Cookie
Origin検証
Fetch Metadata
Partitioned Cookie
サードパーティCookie制限
OAuth・OIDCの厳格化
WebAuthnによる重要操作確認
ブラウザ分離
APIの型とスキーマ検証
リスクベース認証
操作単位の再認証
Passkeyによるトランザクション確認
ただし、ブラウザ側の制限が強くなっても、次の原則は変わりません。
Cookieを持っている
≠
利用者がその操作を意図した
サーバーは認証状態だけでなく、正規画面から開始された操作であること、現在のセッションに対応するトークンであること、必要に応じて利用者が重要操作を再確認したことを検証する必要があります。
26. まとめ
CSRFは、ログイン中のブラウザが認証Cookieを自動送信する性質を悪用し、利用者の意図しない操作を実行させる攻撃です。
主要フレームワークの標準防御やSameSite Cookieによって、単純なフォーム型CSRFは以前より成功しにくくなりました。
しかし、CSRFが完全に解決したわけではありません。
現代では次の攻撃面を確認する必要があります。
GETによる状態変更
不完全なトークン検証
ログインCSRF
JSON・API・SPA
OAuth・OpenID Connect
WebSocket
Client-Side CSRF
Same-Siteの兄弟サブドメイン
CORS誤設定
csrf_exemptの誤用
Djangoでは、次の構成を基本にします。
CsrfViewMiddleware
+
{% csrf_token %}
+
X-CSRFToken
+
Secure Cookie
+
SameSite
+
Origin・Referer検証
+
GETで状態変更しない
+
重要操作の再認証
Django標準機能を無効化せず、テスト、ログ監視、サブドメイン管理、XSS対策まで含めて多層防御を構築することが重要です。
FAQ
Q1. Djangoでは{% csrf_token %}を入れるだけで安全ですか?
通常のDjangoフォームでは重要な基本対策です。
ただし、ミドルウェア、HTTPS、Cookie属性、Origin設定、GET設計、XSS対策も必要です。
Q2. GETリクエストにもCSRFトークンを付けるべきですか?
GETでは状態を変更しない設計にします。
検索や画面表示などの読取りだけに使う限り、通常はCSRFトークンを付けません。
Q3. SameSite=Strictにすればトークンは不要ですか?
不要にはなりません。
StrictでもSame-Siteの兄弟ドメインや正規サイト内のガジェットを利用した攻撃などを考慮する必要があります。
Q4. JWTを使えばCSRFはなくなりますか?
JWTそのものでは決まりません。
JWTをCookieへ保存して自動送信する場合はCSRF対策が必要です。
Authorizationヘッダーへ明示的に付ける場合は古典的CSRFに強くなりますが、XSS対策が重要になります。
Q5. Django REST FrameworkでもCSRFトークンは必要ですか?
SessionAuthenticationを使う場合は必要です。
Cookieを使わないBearer Token方式では典型的なCookie型CSRFは成立しにくくなりますが、トークン保存場所、CORS、XSSを別途確認します。
Q6. csrf_exemptはいつ使えますか?
外部Webhookなど、ブラウザのCookie認証を利用しないエンドポイントで使う場合があります。
その場合もHMAC署名、公開鍵署名、タイムスタンプ、リプレイ防止などの代替認証が必要です。
Q7. CSRFとSSRFは同じですか?
異なります。
CSRFは利用者のブラウザに偽のリクエストを送らせる攻撃です。
SSRFはサーバー自身に内部・外部の別URLへリクエストさせる攻撃です。
Q8. CSRFトークンが漏れたらどうなりますか?
有効なセッションに対応するトークンが攻撃者へ漏れると、防御を突破される可能性があります。
URL、ログ、外部フォーム、Refererへ漏らさず、XSS対策も行います。
Q9. Cloudflare WAFでCSRFを完全に防げますか?
困難です。
WAFはOrigin、Fetch Metadata、レート制限などの補助防御に使えますが、利用者の意図を確認するCSRFトークン検証はアプリケーション側で行います。
Q10. DjangoでCSRF 403が急増したら攻撃ですか?
攻撃とは限りません。
フロントエンド更新、Cookie属性変更、CloudflareやNginxのスキーム判定、CSRF_TRUSTED_ORIGINS不足などでも増加します。
エラーログ、Origin、Referer、URL、デプロイ時刻を照合して判断します。
コメント(0件)
まだコメントはありません。最初のコメントを投稿してください!