できない.dev

Django で「CSRF verification failed. Request aborted.」の 403 が解決できない

CSRF の 403 は「Reason given for failure」で対処が分かれる。CSRF token missing. ならトークンを送り、Origin checking failed なら CSRF_TRUSTED_ORIGINS かプロキシの HTTPS 判定を見直す。

公開:

要約

CSRF verification failed. Request aborted. は、CsrfViewMiddleware が POST などの安全でないリクエストを拒否したときの 403 だ。DEBUG = True なら画面の「Reason given for failure:」に理由が 1 行で出る。
ログにも同じ理由が残る。

Forbidden (CSRF token missing.): /form/
Forbidden (Origin checking failed - http://localhost:3000 does not match any trusted origins.): /api/items/

CSRF token missing. / CSRF cookie not set. はトークンの受け渡し、Origin checking failed はオリジンの設定が原因だ。

よくある原因

  1. フォームにトークンが無い: POST では、CSRF クッキーに加えて csrfmiddlewaretoken フィールド(または後述の X-CSRFToken ヘッダ)が必要になる。
    テンプレートに {% csrf_token %} を書き忘れると CSRF token missing. になる。
  2. JavaScript からの POST にヘッダが無い: JSON を送る fetch はフォームのフィールドを持たないので、X-CSRFToken ヘッダが無いと同じエラーになる。
  3. クッキーがまだ無い: CSRF クッキーは get_token() が呼ばれたとき(トークンを出力するページを返したとき)に発行される。
    いきなり POST すると CSRF cookie not set. になる。
  4. Origin が Host と一致しない: Django 4.0 以降は Origin ヘッダを Host と CSRF_TRUSTED_ORIGINS に照合する。http://localhost:3000 の開発サーバから localhost:8000 の Django へ送る場合や、HTTPS を終端したプロキシが HTTP で Django に転送する場合(Origin は https://、Django 側は http:// と判断する)に一致しない。
  5. スキームの無い CSRF_TRUSTED_ORIGINS: Django 4.0 で値の形式が変わり、ホスト名だけを書くとシステムチェックで起動が止まる。

解決策

1. フォームに csrf_token を書く

<form method="post">
  {% csrf_token %}
  <button type="submit">送信</button>
</form>

ビューは render() で返し、テンプレートにリクエストを渡す。

2. fetch では X-CSRFToken ヘッダを付ける

公式ドキュメントが推奨する取得元は csrftoken クッキーだ。

function getCookie(name) {
  const m = document.cookie.match(new RegExp("(^|;\\s*)" + name + "=([^;]*)"));
  return m ? decodeURIComponent(m[2]) : null;
}
 
fetch("/api/items/", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-CSRFToken": getCookie("csrftoken"),
  },
  body: JSON.stringify({ name: "dekinai" }),
  mode: "same-origin",
});

CSRF_COOKIE_HTTPONLY を有効にしている場合は JavaScript からクッキーを読めないので、ページに {% csrf_token %} を出力して DOM から値を読む。

3. 先にクッキーを発行する

トークンを出力するテンプレートを経由しない構成では、最初に開くビューに ensure_csrf_cookie を付ける。

from django.views.decorators.csrf import ensure_csrf_cookie
 
@ensure_csrf_cookie
def index(request):
    ...

4. 信頼するオリジンを設定する

別オリジンから送るなら、送信元をスキーム付きで登録する。

CSRF_TRUSTED_ORIGINS = ["http://localhost:3000", "https://app.example.com"]

HTTPS を終端するプロキシの裏で動かす場合は、プロキシが付ける X-Forwarded-Proto を Django に信頼させる方法もある。

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

ただし、プロキシがこのヘッダを必ず設定し、クライアントから届いた同名ヘッダを取り除く構成でない限り使ってはならないと公式ドキュメントが警告している。

5. スキームを付けて書き直す

次のエラーが出たら、値に https:// か http:// を付ける。

?: (4_0.E001) As of Django 4.0, the values in the CSRF_TRUSTED_ORIGINS setting must start with a scheme (usually http:// or https://) but found app.example.com. See the release notes for details.

旧形式の ".example.com" は "https://*.example.com" に書き換える。@csrf_exempt で検査ごと外すと CSRF 対策そのものが無くなるので、原因を直すのが先だ。

この記事は役立ちましたか?