こんにちは、かつコーチです。
「フォームを送信したら403 Forbiddenが出た」「CSRFトークンって何のために必要なの?」という質問は、Djangoでフォームを扱い始めた頃に必ずと言っていいほど出てくる悩みです。
この記事では、Djangoが標準で備えているCSRF対策の仕組みと、settings.pyのMIDDLEWAREに並ぶセキュリティ関連ミドルウェアの役割を整理します。
エラーが出たときに「なぜ守られているのか」を理解できると、必要のない箇所でCSRF対策を無効化してしまう事故を防げます。
CSRFとは?なぜDjangoは標準で対策しているのか
CSRF(クロスサイトリクエストフォージェリ)の定義
CSRF(Cross-Site Request Forgery)とは、ログイン済みのユーザーを騙して、本人の意図しないリクエスト(送金や退会処理など)を勝手に送信させてしまう攻撃手法です。
攻撃者は悪意のあるサイトに罠のフォームを仕込み、ユーザーがログイン状態のままそのサイトを開くと、ブラウザに保存されたクッキーを使って正規サイトへ不正なリクエストが送られてしまいます。
DjangoはCSRF対策が最初から有効になっている
Djangoではdjango-admin startprojectした時点で、settings.pyのMIDDLEWAREにdjango.middleware.csrf.CsrfViewMiddlewareが最初から入っています。
つまり、何も意識しなくてもPOSTリクエストに対するCSRF対策が有効になっている状態からスタートします。
このミドルウェアが、フォームに埋め込まれたCSRFトークン(ランダムな文字列)とサーバー側で保持しているトークンを照合し、一致しない場合はリクエストを403 Forbiddenで拒否します。
CSRF対策の基本的な書き方
テンプレートに{% csrf_token %}を入れる
POSTメソッドを使うフォームには、{% csrf_token %}テンプレートタグを必ず入れます。
<!-- templates/contact.html -->
<form method="post">
{% csrf_token %}
<input type="text" name="name">
<button type="submit">送信</button>
</form>
このタグが出力するのは、以下のような隠しフィールドです。
<input type="hidden" name="csrfmiddlewaretoken" value="ランダムな文字列">
Ajax(fetch/axios)でPOSTする場合の対応
テンプレート以外から、JavaScriptのfetchでPOSTリクエストを送る場合は、CookieからCSRFトークンを取り出してヘッダーに乗せる必要があります。
// static/js/main.js
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}
fetch('/api/comments/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken'),
},
body: JSON.stringify({ text: 'コメント本文' }),
});
X-CSRFTokenヘッダーは、CsrfViewMiddlewareが自動的に検証してくれる名前です。
ヘッダー名を独自のものに変える場合はCSRF_HEADER_NAMEの設定変更が必要になるので、基本はこの標準名を使うのが無難です。
よくあるつまずきポイント・エラー対処
❌ Before:@csrf_exemptで安易に回避してしまう
CSRFトークンが合わずにエラーが出たとき、初学者がやりがちなのが@csrf_exemptデコレータで検証自体を無効化してしまうことです。
# ❌ Before:CSRF検証を丸ごと無効化してしまう
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def contact_view(request):
if request.method == "POST":
# フォーム処理
...
これはエラーが消えるだけで、CSRF攻撃に対する防御をそのビューだけ丸腰にしてしまう危険な対処法です。
外部の決済サービスからのWebhookなど、CSRFトークンを持たせようがない特殊なエンドポイント以外では使うべきではありません。
✅ After:原因を特定してから対処する
筆者が実際にハマったのは、テンプレートを別ファイルに切り出した際に{% csrf_token %}を書き忘れ、以下のエラーに悩まされたケースです。
Forbidden (403)
CSRF verification failed. Request aborted.
このときは@csrf_exemptを貼るのではなく、開発サーバーのデバッグ画面に表示される「Reason given for failure」の内容を確認し、{% csrf_token %}の書き忘れが原因だと突き止めて修正しました。
<!-- ✅ After:忘れていたcsrf_tokenを追加 -->
<form method="post">
{% csrf_token %}
...
</form>
エラーの原因を安易に無効化で片付けず、まず「なぜトークンが一致しないのか」を確認する癖をつけることが、セキュリティを維持したままエラーを解消する近道です。
セキュリティミドルウェアを理解して一歩先へ
SecurityMiddlewareが担う役割
MIDDLEWAREにはCSRF対策以外にもdjango.middleware.security.SecurityMiddlewareが並んでいます。
これは以下のようなHTTPヘッダーの付与・制御をまとめて担当するミドルウェアです。
| 設定項目 | 役割 |
|---|---|
SECURE_SSL_REDIRECT | HTTPアクセスを強制的にHTTPSへリダイレクトする |
SECURE_HSTS_SECONDS | HSTS(HTTPSを強制するブラウザへの指示)の有効期間を設定する |
X_FRAME_OPTIONS | クリックジャッキング対策として、他サイトへのiframe埋め込みを制限する |
SECURE_CONTENT_TYPE_NOSNIFF | ブラウザによるContent-Typeの推測(MIMEスニッフィング)を防ぐ |
本番環境で見直すべき設定
開発中はデフォルトのままで問題ありませんが、本番デプロイ前には以下のような設定をsettings.py(本番用の設定ファイル)で有効にしておくことをおすすめします。
# settings/production.py
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
X_FRAME_OPTIONS = "DENY"
python manage.py check --deployコマンドを実行すると、本番運用の観点で見直すべきセキュリティ設定を一覧で警告してくれます。
デプロイ前のチェックリストとして習慣化しておくと安心です。
まとめ
この記事のポイント
- DjangoはCSRF対策が
startproject直後から有効になっており、{% csrf_token %}とミドルウェアがセットで機能する - Ajax通信では
X-CSRFTokenヘッダーにCookieの値を乗せて送る @csrf_exemptは原因を特定した上での最終手段であり、安易な回避策にしないSecurityMiddlewareはHTTPS強制やクリックジャッキング対策など複数の役割を持つpython manage.py check --deployで本番前のセキュリティ設定を確認する
次に読むべき記事
セキュリティの理解を固めたら、次はテストでその安全性を担保する方法を学びましょう。
「Django TestCaseでモデル・ビューのテストを書く」の記事で、テストの基本を解説しています。
タグ: Django, 中級者向け, セキュリティ