Skip to content

Fix: Django Forbidden (403) CSRF verification failed

FixDevs ·

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). This token proves the request came from your own site, not from a malicious third-party page.

The CSRF check verifies:

  1. A CSRF cookie is set in the browser.
  2. A matching CSRF token is sent in the form data or header.
  3. The Referer header matches a trusted origin (for HTTPS requests).

Common causes:

  • Missing {% csrf_token %} in the form template.
  • AJAX request without the CSRF header.
  • CSRF_TRUSTED_ORIGINS not configured for your domain.
  • 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.

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']

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',
    ...
]

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 %}

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',
    ...
]

For Django database errors, see Fix: Django OperationalError: no such table. For Python import issues, see Fix: Python ModuleNotFoundError: No module named.

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