Guides / CSRF verification failed. Request aborted.

CSRF verification failed. Request aborted.

Forms and logins worked locally. After deploying behind HTTPS, every POST ends in a 403 page that says CSRF verification failed. Request aborted. Django 4.0 and newer check the browser's Origin header against the address it thinks it is served on, and behind a proxy it often has that address wrong. The line under "Reason given for failure" tells you which cause you have.

What it means

Django's CSRF protection rejects a POST unless it carries a token that matches a cookie, and, on HTTPS, comes from an origin Django trusts. The generic message is the same for every failure. The reason is the useful part. With DEBUG = True it is on the page. In production it is in your logs, under the django.security.csrf logger:

Forbidden (Origin checking failed - https://example.com does not match any trusted origins.): /accounts/login/

Why it happens

Match the reason to the cause. They are listed from most to least common after a first deploy.

  1. "Origin checking failed - https://example.com does not match any trusted origins." You are behind a proxy or load balancer that ends HTTPS. The browser sends Origin: https://example.com, but the proxy talks to Django over plain http, so Django believes the site is http://example.com and the two don't match.
  2. "CSRF cookie not set." The browser did not send the CSRF cookie back. Typical reasons: CSRF_COOKIE_SECURE = True while the user is on http:// (mixed http and https), a CSRF_COOKIE_DOMAIN that does not match the domain, or a CDN or page cache that served a cached copy of the form.
  3. "Referer checking failed - no Referer." On HTTPS Django also checks the Referer header. A Referrer-Policy: no-referrer header, set by your proxy or by SECURE_REFERRER_POLICY, strips it.
  4. "CSRF token missing." The form has no {% csrf_token %}, or your JavaScript posts without the X-CSRFToken header.
  5. Posts from another origin, such as a separate front-end domain, a subdomain, or an admin on a different host. Those origins have to be trusted explicitly.

The fix

1. Behind a proxy: tell Django the request was HTTPS

This is the real fix for the first cause, and it also fixes request.is_secure(), the scheme in build_absolute_uri() and secure cookies. Set SECURE_PROXY_SSL_HEADER to the header your proxy sets:

# settings.py
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

Only do this if the proxy always sets or overwrites that header. If clients can send their own X-Forwarded-Proto, they can pretend to be secure. With nginx in front, add this to the location block that proxies to Django:

proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;

2. Trust your origins explicitly

Add your site's origins to CSRF_TRUSTED_ORIGINS. Since Django 4.0 every entry must include the scheme. A bare example.com stops Django from starting with (4_0.E001) As of Django 4.0, the values in the CSRF_TRUSTED_ORIGINS setting must start with a scheme.

CSRF_TRUSTED_ORIGINS = [
    "https://example.com",
    "https://www.example.com",
    "https://*.example.com",  # every subdomain, if you need it
]

This is also what you need for a front end on another origin, and when a proxy rewrites the Host header so that fix 1 alone isn't enough. A trusted origin only affects the Origin and Referer checks; the token and cookie are still required.

3. Mixed http and https: send everyone to https

If the cookies are secure-only, people on http:// will never get a working form. Redirect to https, and mark the cookies secure once that works:

SECURE_SSL_REDIRECT = True          # needs fix 1, or it loops forever
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True

Set SECURE_PROXY_SSL_HEADER first. Without it Django sees every request as http, redirects it to https, and the browser ends up in a redirect loop.

4. Cookie, token and referrer problems

Making it stick in production

Read the origins from the environment so staging and production don't share a list:

import os

CSRF_TRUSTED_ORIGINS = [
    origin.strip()
    for origin in os.environ.get("DJANGO_CSRF_TRUSTED_ORIGINS", "").split(",")
    if origin.strip()
]
export DJANGO_CSRF_TRUSTED_ORIGINS="https://example.com,https://www.example.com"

Then run python manage.py check --deploy. It flags a missing SECURE_SSL_REDIRECT, insecure session and CSRF cookies, and missing HSTS (deployment checklist). To see what Django thinks of a request, temporarily log request.is_secure() and request.get_host() in a view: behind a correct setup they are True and your public domain.

If the 403 is replaced by a 400 page about the host, see DisallowedHost: Invalid HTTP_HOST header.

Questions

Do I need CSRF_TRUSTED_ORIGINS if SECURE_PROXY_SSL_HEADER is set?

Usually not. With the header set, Django sees the request as https and the Origin matches the Host. You still need CSRF_TRUSTED_ORIGINS when the form is posted from a different origin, such as a separate front end, or when the proxy rewrites the Host header.

Can I just disable CSRF protection?

No. The check protects logged-in users from forms submitted by other sites. Use @csrf_exempt only on views that do not rely on cookies, such as a webhook that verifies its own signature.

Or skip it

Django Cloud ends HTTPS at the load balancer and sets SECURE_PROXY_SSL_HEADER for you when your settings don't, and it adds the https:// address of your app and of each custom domain to CSRF_TRUSTED_ORIGINS. Forms keep working on the first deploy without touching either setting.

pip install djangocloud-cli
djangocloud deploy

Get startedSee pricing

Free plans are available for students, AWS Activate startups, open-source, non-profit and research projects. See how Django Cloud compares with other ways to host Django.

Django is a registered trademark of the Django Software Foundation. Django Cloud is not affiliated with or endorsed by it. Found a mistake in this guide? Tell us and we will fix it.