事象
本番環境(VPS / Nginx / Dockerなど)でDjangoアプリケーションをHTTPS化して公開したところ、ログイン画面や管理画面、フォーム送信(POSTリクエスト)時に次の403エラー画面が表示された。
403 Forbidden
アクセス禁止 (403)
CSRF検証に失敗したため、リクエストは中断されました。
ALLOWED_HOSTS にドメインを設定しており、GETリクエストによるページ表示やHTTPSアクセス自体は問題なく行えているにもかかわらず、POST送信時のみエラーが発生する。
環境
- Python 3.x
- Django 4.0以降(Django 4.0でCSRFの仕様変更あり)
- Nginx(リバースプロキシ)
- Docker / Docker Compose
- HTTPS(独自ドメイン)
原因
主な原因は、settings.py の CSRF_TRUSTED_ORIGINS にHTTPSのオリジンが登録されていないことである。
なぜ発生するのか
-
Django 4.0以降の仕様変更
Django 4.0以降、HTTPS環境下でのCSRF検証において、リクエストのOriginヘッダー(またはRefererヘッダー)がCSRF_TRUSTED_ORIGINSに登録されているかを厳格に検証するようになった。 -
ALLOWED_HOSTSとCSRF_TRUSTED_ORIGINSの混同-
ALLOWED_HOSTS: リクエストのHostヘッダーを検証する設定。スキーム(https://)や末尾のスラッシュは含めない(例:example.com)。 -
CSRF_TRUSTED_ORIGINS: CSRF検証で信頼する送信元を指定する設定。スキーム(https://やhttp://)を含める必要がある(例:https://example.com)。
-
ALLOWED_HOSTS だけを設定して CSRF_TRUSTED_ORIGINS を設定していない場合、GETアクセスは通るがPOST送信時のCSRF検証で弾かれてしまう。
対策
1. settings.py に CSRF_TRUSTED_ORIGINS を追加する
settings.py を開き、公開しているドメインのオリジン(スキーム付き)を追加する。
# settings.py
# Hostヘッダーの許可(スキームなし)
ALLOWED_HOSTS = [
"example.com",
"www.example.com",
]
# CSRFの信頼済みオリジン(スキームあり・末尾スラッシュなし)
CSRF_TRUSTED_ORIGINS = [
"https://example.com",
"https://www.example.com",
]
サブドメインを含めてまとめて許可したい場合は、次のようにワイルドカード指定も可能。
CSRF_TRUSTED_ORIGINS = [
"https://*.example.com",
]
2. Nginxのリバースプロキシ設定を確認する
リバースプロキシ(Nginx)経由でリクエストを渡している場合、HTTPS通信であることをDjango側に正しく伝えるヘッダーが抜けていないか確認する。
# nginx.conf (または sites-available/*.conf)
location / {
proxy_pass http://127.0.0.1:8000;
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; # これが必要
}
Nginxの設定を変更した場合はリロードする。
sudo nginx -t && sudo systemctl reload nginx
3. Djangoアプリケーション(Dockerコンテナ)を再起動する
settings.py の変更を反映させるため、Djangoのプロセス(またはDockerコンテナ)を再起動する。
# Docker Composeの場合
docker compose restart web
※ NginxのリロードだけではDjango側の設定変更は反映されないため注意。
4. 動作確認
ブラウザからログイン画面やフォームを開き、POST送信を行ってエラーが出ずに処理が完了することを確認する。