Skip to content

Fix: Django Forbidden (403) CSRF verification failed

FixDevs · (Updated: )

Part of:  Python Errors

Quick Answer

How to fix Django 403 CSRF verification failed error caused by missing CSRF tokens, AJAX requests, cross-origin issues, HTTPS misconfig, and session problems.

The Error

Your Django form submission or POST request fails with:

Forbidden (403)
CSRF verification failed. Request aborted.

Reason given for failure:
    CSRF token missing or incorrect.

Or variations:

Forbidden (403)
CSRF verification failed. Request aborted.
Reason given for failure:
    CSRF cookie not set.
Forbidden (403)
CSRF verification failed. Request aborted.
Reason given for failure:
    Referer checking failed - https://example.com does not match any trusted origins.

Django’s Cross-Site Request Forgery protection rejected the request because it could not verify that the request is legitimate and not a forged attack.

Why This Happens

Django requires a CSRF token on every POST, PUT, PATCH, and DELETE request (any state-changing request). The token proves the request originated from a page your own server rendered, not from a malicious cross-site form submission. Without this defense, a logged-in user visiting an attacker’s page could be tricked into submitting authenticated requests to your site.

The CSRF check verifies:

  1. A CSRF cookie is set in the browser (the csrftoken cookie, set by CsrfViewMiddleware).
  2. A matching CSRF token is sent in the form data (csrfmiddlewaretoken) or in the X-CSRFToken header for AJAX.
  3. The Referer header matches a trusted origin (for HTTPS requests). Django uses Referer-checking on HTTPS specifically because mixed-content downgrades and certain cookie-stealing attacks become harder when Referer is enforced.

Django splits the failure reasons in the response text precisely so you can debug them: “CSRF token missing or incorrect” means the header/form field is wrong, “CSRF cookie not set” means no cookie made it to the server, and “Referer checking failed” means the cookie and token are fine but the request origin is not on your allow-list. Each requires a different fix.

Common causes:

  • Missing {% csrf_token %} in the form template.
  • AJAX request without the CSRF header.
  • CSRF_TRUSTED_ORIGINS not configured for your domain (Django 4.0+).
  • Cookie not set. The csrftoken cookie was blocked or expired.
  • HTTPS/HTTP mismatch. The request origin does not match the trusted origins.
  • Caching issue. A cached page serves a stale CSRF token.
  • Cross-origin request. The frontend and backend are on different domains, and SameSite=Lax prevents the cookie from being sent.

Platform and Environment Differences

CSRF in Django depends on cookie behavior, and cookie behavior is the most environment-sensitive part of the web platform. Browser version, OS keychain integration, reverse proxies, mobile WebViews, and dev-vs-prod URL schemes all change how cookies arrive at the server.

SameSite cookie defaults across browsers. Chrome 80 (February 2020) changed the default SameSite for cookies without an explicit attribute from None to Lax. Safari 13.1 followed. Firefox enabled it in 2020 as well. Django’s CSRF_COOKIE_SAMESITE defaulted to 'Lax' from Django 2.1. If you have an old Django (1.x) on a modern browser, the cookie is rejected on cross-site POSTs unless SameSite=None; Secure is explicit.

Chrome 84+ rejects SameSite=None without Secure. Setting CSRF_COOKIE_SAMESITE = 'None' requires CSRF_COOKIE_SECURE = True. Otherwise Chrome drops the cookie silently. Safari 14+ enforces the same rule. In development on HTTP, the safer choice is 'Lax' and serving frontend on the same origin.

Safari ITP (Intelligent Tracking Prevention). Safari treats third-party cookies as ephemeral after 7 days, even if Secure and SameSite=None. A user who returns to your site after a week may need to re-fetch a CSRF cookie. This matters most for SPAs that bootstrap with a token fetch.

Firefox Total Cookie Protection. Firefox partitions third-party cookies by top-level site since 2022. A widget hosted under widget.example.com and embedded in customer-a.com and customer-b.com gets two different cookie jars. Your CSRF cookie does not cross between them.

Mobile WebViews. Apps that render web pages in a WKWebView (iOS) or WebView (Android) sometimes block cookies entirely if the app forgot to enable the cookie store. Apps embedding your site in an OAuth flow may strip cookies on redirect. Test on real device WebView, not just mobile Safari.

HTTPS in production vs HTTP in dev. CSRF_COOKIE_SECURE = True only sends the cookie over HTTPS. If your local dev runs on http://localhost:8000 with CSRF_COOKIE_SECURE = True, the cookie never reaches the browser and every POST fails. Gate it on DEBUG: CSRF_COOKIE_SECURE = not DEBUG. Mirror the setting for SESSION_COOKIE_SECURE and SECURE_SSL_REDIRECT.

CSRF_TRUSTED_ORIGINS — Django 4.0+ requirement. Pre-4.0 Django inferred trusted origins from the request host. From 4.0 onward you must list every origin explicitly with scheme, including https://*.example.com if you want subdomain wildcards. Forgetting this is the single most common cause of “Referer checking failed” in Django upgrades. Use https://, not :// or bare hostnames.

Reverse proxies (Nginx, HAProxy, ELB). A proxy that terminates SSL and forwards plaintext to Django makes request.is_secure() return False. Django then skips Referer checks but also sees the wrong scheme in CSRF_TRUSTED_ORIGINS comparisons. Set SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') and ensure the proxy sets that header. Cloudflare and AWS ALB set it automatically; Nginx requires proxy_set_header X-Forwarded-Proto $scheme;.

Subdomain sharing (CSRF_COOKIE_DOMAIN = '.example.com'). Sharing the cookie across app.example.com and api.example.com works only if both are real subdomains of the same registrable domain. localhost cannot share cookies with 127.0.0.1 because they are separate origins. For local cross-subdomain testing, use a tool like dnsmasq or lvh.me (resolves *.lvh.me to 127.0.0.1).

Browser private/incognito modes. Cookies set during a private session are wiped on close. A user who completes a multi-step form across reopened windows may lose the CSRF cookie. Not a Django bug, but a real support ticket source.

Same-origin SPA in development with Vite/CRA. A React SPA on localhost:3000 posting to Django on localhost:8000 is cross-origin. The browser sees two different ports as two different origins. You need django-cors-headers plus CSRF_TRUSTED_ORIGINS = ['http://localhost:3000'] plus CSRF_COOKIE_SAMESITE = 'Lax' (or 'None' over HTTPS) plus CORS_ALLOW_CREDENTIALS = True.

Fix 1: Add CSRF Token to Forms

The most common fix. Every HTML form that uses POST needs the token:

Broken:

<form method="post" action="/submit/">
    <input type="text" name="message">
    <button type="submit">Send</button>
</form>

Fixed:

<form method="post" action="/submit/">
    {% csrf_token %}
    <input type="text" name="message">
    <button type="submit">Send</button>
</form>

{% csrf_token %} renders a hidden input field:

<input type="hidden" name="csrfmiddlewaretoken" value="abc123...">

Pro Tip: Always include {% csrf_token %} in every <form method="post"> in your Django templates. GET forms do not need it because GET requests should not change server state.

Fix 2: Fix AJAX/Fetch Requests

JavaScript requests need to send the CSRF token in a header:

Using fetch:

// Get the CSRF token from the cookie
function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}

fetch('/api/data/', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRFToken': getCookie('csrftoken'),
    },
    body: JSON.stringify({ message: 'hello' }),
});

Using Axios (global setup):

import axios from 'axios';

axios.defaults.xsrfCookieName = 'csrftoken';
axios.defaults.xsrfHeaderName = 'X-CSRFToken';

// Now all requests automatically include the CSRF token
axios.post('/api/data/', { message: 'hello' });

Using jQuery:

$.ajaxSetup({
    beforeSend: function(xhr, settings) {
        if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
        }
    }
});

Common Mistake: Forgetting that the CSRF cookie must exist before you can read it. If the user has not loaded any Django page yet, the cookie may not be set. Use the ensure_csrf_cookie decorator on a view to force-set the cookie.

Fix 3: Configure CSRF_TRUSTED_ORIGINS

For Django 4.0+, you must list trusted origins explicitly:

# settings.py
CSRF_TRUSTED_ORIGINS = [
    'https://example.com',
    'https://www.example.com',
    'https://app.example.com',
]

# For development
CSRF_TRUSTED_ORIGINS = [
    'http://localhost:3000',
    'http://localhost:8000',
    'http://127.0.0.1:8000',
]

The “Referer checking failed” error specifically means CSRF_TRUSTED_ORIGINS is missing or does not include the requesting origin.

Include the scheme (http/https):

# Wrong — missing scheme
CSRF_TRUSTED_ORIGINS = ['example.com']

# Correct
CSRF_TRUSTED_ORIGINS = ['https://example.com']

Wildcards work for subdomains but not for schemes: https://*.example.com matches app.example.com but not the bare example.com. List both if you serve both.

Fix 4: Fix Cross-Origin Frontend/Backend

When your React/Vue/Angular frontend is on a different origin than Django:

# settings.py

# Allow the frontend origin
CORS_ALLOWED_ORIGINS = [
    'http://localhost:3000',  # React dev server
]

CSRF_TRUSTED_ORIGINS = [
    'http://localhost:3000',
]

# Allow credentials (cookies) in cross-origin requests
CORS_ALLOW_CREDENTIALS = True

# Ensure CSRF cookie is accessible cross-origin
CSRF_COOKIE_SAMESITE = 'Lax'  # or 'None' for cross-site (requires Secure)
CSRF_COOKIE_HTTPONLY = False   # JavaScript needs to read it
SESSION_COOKIE_SAMESITE = 'Lax'

Install django-cors-headers:

pip install django-cors-headers
INSTALLED_APPS = [
    'corsheaders',
    ...
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # Must be before CommonMiddleware
    'django.middleware.common.CommonMiddleware',
    ...
]

On the frontend, set credentials: 'include' in fetch or withCredentials: true in Axios. Without this, the browser does not send the CSRF cookie cross-origin and the request fails before it reaches Django’s CSRF check.

Fix 5: Fix for API Views (DRF)

Django REST Framework can use different authentication that does not need CSRF:

For token-based authentication:

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
        # SessionAuthentication requires CSRF — remove it if using tokens only
    ],
}

Exempt specific views from CSRF:

from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def webhook_view(request):
    """External webhook — no CSRF token available."""
    # Verify the request authenticity another way (signature, API key)
    pass

For class-based views:

from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt

@method_decorator(csrf_exempt, name='dispatch')
class WebhookView(View):
    def post(self, request):
        pass

Warning: Only use @csrf_exempt for views that have alternative authentication (API keys, webhook signatures, token auth). Never exempt regular form views.

If the error says “CSRF cookie not set”:

Force the cookie to be set:

from django.views.decorators.csrf import ensure_csrf_cookie

@ensure_csrf_cookie
def get_csrf_token(request):
    """Set the CSRF cookie for JavaScript clients."""
    return JsonResponse({'status': 'ok'})

Check cookie settings:

# settings.py

# Cookie name (default: 'csrftoken')
CSRF_COOKIE_NAME = 'csrftoken'

# For HTTPS only
CSRF_COOKIE_SECURE = True  # Only sent over HTTPS

# Allow JavaScript to read the cookie
CSRF_COOKIE_HTTPONLY = False  # Must be False for AJAX

# Cookie domain
CSRF_COOKIE_DOMAIN = '.example.com'  # Share across subdomains

# SameSite attribute
CSRF_COOKIE_SAMESITE = 'Lax'  # Default, good for most cases

Common issue — CSRF_COOKIE_SECURE = True on HTTP:

# This blocks the cookie on HTTP (development)
CSRF_COOKIE_SECURE = True

# Fix: Only enable in production
CSRF_COOKIE_SECURE = not DEBUG

Fix 7: Fix Caching Issues

Cached pages can serve stale CSRF tokens:

# Never cache pages with forms
from django.views.decorators.cache import never_cache

@never_cache
def my_form_view(request):
    return render(request, 'form.html')

With template fragment caching:

{# Don't cache the form part #}
<form method="post">
    {% csrf_token %}
    {# The rest of the form #}
</form>

{# Cache other parts #}
{% cache 600 sidebar %}
    {# Sidebar content #}
{% endcache %}

CDNs like Cloudflare and Fastly cache HTML by default for certain content types. Even with never_cache, an edge layer can serve a page rendered with a different user’s token if cache rules are too aggressive. Add Cache-Control: private, no-store to all pages that render a CSRF token.

Fix 8: Debug CSRF Failures

Enable detailed CSRF failure logging:

# settings.py
LOGGING = {
    'version': 1,
    'handlers': {
        'console': {'class': 'logging.StreamHandler'},
    },
    'loggers': {
        'django.security.csrf': {
            'handlers': ['console'],
            'level': 'DEBUG',
        },
    },
}

Custom CSRF failure view for debugging:

# settings.py
CSRF_FAILURE_VIEW = 'myapp.views.csrf_failure'

# myapp/views.py
def csrf_failure(request, reason=""):
    from django.http import JsonResponse
    return JsonResponse({
        'error': 'CSRF verification failed',
        'reason': reason,
        'cookie_present': bool(request.COOKIES.get('csrftoken')),
        'origin': request.META.get('HTTP_ORIGIN', 'none'),
        'referer': request.META.get('HTTP_REFERER', 'none'),
    }, status=403)

Still Not Working?

Check for reverse proxy headers. If behind Nginx or a load balancer:

# settings.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
USE_X_FORWARDED_HOST = True

Check for browser privacy settings. Some browsers block third-party cookies, which can affect CSRF cookies in cross-origin setups.

Check for middleware order. CsrfViewMiddleware must be in the correct position:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',       # Before CommonMiddleware
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',   # After SessionMiddleware
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    ...
]

Check the cookie path. If you set CSRF_COOKIE_PATH = '/admin' and post to /api/, the cookie does not get sent. Leave the path at the default / unless you have a deliberate reason to scope it.

Check for a misconfigured ALLOWED_HOSTS. Django blocks requests whose Host header does not match ALLOWED_HOSTS before the CSRF check runs. The user may see a 400 in the browser log and a 403 CSRF error in the server log because retries hit different pods. Verify both settings are correct in every environment.

Check session backend integrity. CSRF tokens for authenticated sessions are tied to the session key. If you rotated SECRET_KEY recently, old sessions cannot validate their tokens. Either log users out (Session.objects.all().delete()) or pin SECRET_KEY_FALLBACKS to the previous value during a grace period.

Check that the form does not POST through a redirect. A 301/302 redirect from HTTP to HTTPS strips the request body and drops headers, so the second request looks like a GET with no CSRF token. Always submit forms directly to the HTTPS endpoint.

For Django database errors, see Fix: Django OperationalError: no such table. For Python import issues, see Fix: Python ModuleNotFoundError: No module named. For DRF permission issues, see Fix: Django REST Framework permission denied. For browser-side CORS failures, see Fix: CORS preflight request blocked.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles