CSRFとは?歴史・攻撃の種類・トークン防御・Djangoでの守り方を徹底解説

CSRFとは?歴史・攻撃の種類・トークン防御・Djangoでの守り方を徹底解説
目次

はじめに


クロスサイトリクエストフォージェリ(Cross-Site Request Forgery:CSRF)は、ログイン中の利用者になりすまして、パスワード変更、メールアドレス変更、送金、設定変更などを実行させる攻撃です。

攻撃者がパスワードやセッションCookieを盗むとは限りません。

利用者のブラウザが、ログイン済みサイトへCookieを自動送信する性質を悪用します。

主要ブラウザのSameSite Cookieや、Djangoなどのフレームワーク標準防御によって、昔ながらの単純なCSRFは成功しにくくなりました。しかし、CSRFが消滅したわけではありません。

GETによる状態変更、不完全なトークン検証、SameSiteの誤設定、OAuthログイン、サブドメイン侵害、WebSocket、クライアントサイドCSRFなど、現代のWeb構成に合わせた攻撃面が残っています。

この記事では、CSRFの歴史、種類、成立条件、防御方式、Djangoでの実装、確認方法、再発防止まで順番に解説します。


結論・要点

CSRF対策の結論は、1つの仕組みに依存せず、複数の防御を重ねることです。

特に重要なのは、次の構成です。

  1. 状態を変更する処理をGETで実行しない

  2. DjangoのCsrfViewMiddlewareを有効にする

  3. HTMLフォームに{% csrf_token %}を入れる

  4. JavaScriptから送信する場合はX-CSRFTokenヘッダーを付ける

  5. セッションCookieとCSRF CookieにSecureと適切なSameSiteを設定する

  6. OriginおよびRefererの検証を無効化しない

  7. CSRF_TRUSTED_ORIGINSを必要最小限にする

  8. XSS、サブドメイン、CORS、OAuth、WebSocketも一緒に確認する

  9. 送金や認証情報変更では再認証やワンタイム確認を追加する

  10. 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認可フローでは、攻撃者が自分の認可レスポンスを被害者のブラウザへ注入し、アカウントの対応関係を入れ替える攻撃が問題になります。

主な防御は次のとおりです。

  • state

  • nonce

  • PKCE

  • 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

比較CSRFXSS
主な目的利用者に操作させる正規サイト上でスクリプトを動かす
レスポンスの読取り通常は困難可能
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">

リクエスト時に、フォームの値とサーバー側セッションの値を比較します。

長所

  • セッションへ確実に結び付けられる

  • 他ユーザーのトークンを流用しにくい

  • 実装モデルが分かりやすい

短所

  • サーバー側に状態を保存する

  • 複数タブや戻る操作との調整が必要な場合がある

  • リクエストごとに使い捨てると操作性が悪化する場合がある


同じランダム値をCookieとリクエストパラメータまたはヘッダーに入れ、両方が一致するか確認します。

Cookie:
csrf=XYZ123

Header:
X-CSRF-Token: XYZ123

攻撃者サイトはCookieを送信させることはできても、そのCookie値を読み取ってカスタムヘッダーへコピーできない、という性質を利用します。

ただし、単純に値が一致するだけの方式は、サブドメインなどからCookieを設定できる場合に破られる可能性があります。


Double Submit Cookieの値へHMACなどの署名を付け、認証セッションと結び付けます。

OWASPは、単純なDouble Submit Cookieよりも、セッションへ明示的に結び付けたSigned Double Submit Cookieを推奨しています。

概念的には次のような構造です。

token = random + HMAC(server_secret, session_id + random)

サーバーは次を確認します。

  1. Cookieとヘッダーの値が一致する

  2. HMAC署名が正しい

  3. 現在のセッションに結び付いている

独自実装は比較処理、エンコード、鍵管理、トークン固定などのミスが起きやすいため、フレームワーク標準機能を優先します。


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-Sitesame-origin、same-site、cross-site、none
Sec-Fetch-Modenavigate、cors、no-cors、same-origin、websocketなど
Sec-Fetch-Destdocument、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_SECURETrueHTTPSだけでCSRF Cookieを送る
SESSION_COOKIE_SECURETrueHTTPSだけでセッションCookieを送る
CSRF_COOKIE_SAMESITELaxまたはStrictクロスサイト送信を制限
SESSION_COOKIE_SAMESITELaxまたはStrictセッションCookieを制限
SESSION_COOKIE_HTTPONLYTrueJavaScriptによるセッションCookie読取りを防ぐ
CSRF_TRUSTED_ORIGINS最小限信頼する送信元Origin
SECURE_SSL_REDIRECTTrueHTTPSへ強制
HSTS段階導入HTTPSダウングレードを防ぐ

Django 6.0ではSESSION_COOKIE_SAMESITEのデフォルト値はLaxです。

HSTSの注意

SECURE_HSTS_INCLUDE_SUBDOMAINSSECURE_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環境では、少なくとも次を確認します。

  • sessionidSecure

  • sessionidHttpOnly

  • sessionidSameSite

  • csrftokenSecure

  • csrftokenSameSite


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=None

  • CORS誤設定

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. 正規フォームの確認

  1. 正規画面を開く

  2. csrftoken Cookieを確認する

  3. HTML内にcsrfmiddlewaretokenがあることを確認する

  4. 正常にPOSTできることを確認する

  5. 更新結果を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

注意事項

本番環境で直接アップグレードしないでください。

次の順番で進めます。

  1. DBバックアップ

  2. 現在の依存関係を保存

  3. 検証ブランチ作成

  4. テスト環境でアップグレード

  5. Djangoリリースノート確認

  6. 自動テスト

  7. CSRF・ログイン・管理画面テスト

  8. ステージング確認

  9. ロールバック手順作成

  10. 本番反映


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、デプロイ時刻を照合して判断します。


参考情報

読んだ内容を10問練習と実技で確認

記事で理解した用語を、StudyQuestの演習とクラウド実技ラボで定着させます。

10問練習 実技ラボ

コメント(0件)

まだコメントはありません。最初のコメントを投稿してください!

コメントを投稿