Django CSRF Verification Failed: Complete Troubleshooting Guide
If you're building web applications with Django, you've likely encountered this error:
Forbidden (403)
CSRF verification failed. Request aborted.
This error can be confusing, especially when you're sure your form is set up correctly. Let's break down what CSRF protection is, why Django enforces it, and how to fix common issues.
What is CSRF?
CSRF (Cross-Site Request Forgery) is a type of web attack where a malicious website tricks your browser into making unwanted requests to another website where you're logged in.
Example Attack Scenario:
- You're logged into your bank at
bank.com - You visit a malicious site
evil.com evil.comhas a hidden form that submits tobank.com/transfer- Your browser automatically sends your
bank.comcookies - Money gets transferred without your knowledge!
Django's CSRF protection prevents this by requiring a secret token that only your legitimate forms would have.
How Django's CSRF Protection Works
Django protects against CSRF attacks using a token-based system:
- Server generates a token when rendering a form
- Token is embedded in the form as a hidden field
- Client submits the form with the token
- Server validates the token matches what it expects
- Request is accepted only if token is valid
Common Causes and Solutions
Cause 1: Missing {% csrf_token %} in Template
The most common issue - forgetting to include the CSRF token in your form.
<!-- WRONG: Missing CSRF token -->
<form method="post" action="/submit/">
<input type="text" name="username">
<button type="submit">Submit</button>
</form>
Fix: Add {% csrf_token %}
<!-- CORRECT -->
<form method="post" action="/submit/">
{% csrf_token %}
<input type="text" name="username">
<button type="submit">Submit</button>
</form>
This generates a hidden field like:
<input type="hidden" name="csrfmiddlewaretoken" value="abc123...">
Cause 2: AJAX Requests Without CSRF Token
Making POST requests via JavaScript without including the CSRF token.
// WRONG: AJAX POST without CSRF token
fetch('/api/submit/', {
method: 'POST',
body: JSON.stringify(\{data: 'value'\})
})
.then(response => response.json());
// 403 Forbidden - CSRF verification failed
Fix Option 1: Include token in request headers
// CORRECT: Get CSRF token from 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;
}
const csrftoken = getCookie('csrftoken');
// Include in fetch request
fetch('/api/submit/', \{
method: 'POST',
headers: \{
'Content-Type': 'application/json',
'X-CSRFToken': csrftoken
\},
body: JSON.stringify(\{data: 'value'\})
\});
Fix Option 2: Use jQuery (automatically handles CSRF)
// CORRECT: jQuery with CSRF setup
function getCookie(name) {
// ... (same as above)
}
const csrftoken = getCookie('csrftoken');
$.ajaxSetup(\{
beforeSend: function(xhr, settings) \{
if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) \{
xhr.setRequestHeader("X-CSRFToken", csrftoken);
\}
\}
\});
// Now all AJAX requests include the token
$.post('/api/submit/', \{data: 'value'\});
Fix Option 3: Django's csrf.js (modern approach)
<!-- In your template -->
<script>
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
</script>
<script>
// Use in fetch
fetch('/api/submit/', \{
method: 'POST',
headers: \{
'X-CSRFToken': csrftoken
\},
body: JSON.stringify(\{data: 'value'\})
\});
</script>
Cause 3: Middleware Not Enabled
CSRF middleware is missing or disabled in settings.
# settings.py
# WRONG: CSRF middleware commented out or missing
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware', # MISSING!
'django.contrib.auth.middleware.AuthenticationMiddleware',
...
]
Fix: Ensure CSRF middleware is enabled
# CORRECT
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', # Must be here!
'django.contrib.auth.middleware.AuthenticationMiddleware',
...
]
Cause 4: Domain Mismatch or HTTPS Issues
CSRF tokens are domain-specific. Issues arise with:
- Local development vs production domains
- HTTP vs HTTPS mismatches
- Subdomain differences
# Problem: Token generated on http://example.com
# but form submitted from https://example.com
Fix: Configure trusted origins for HTTPS
# settings.py
# For Django 4.0+
CSRF_TRUSTED_ORIGINS = [
'https://yourdomain.com',
'https://*.yourdomain.com', # Include subdomains
]
# For local development with different ports
if DEBUG:
CSRF_TRUSTED_ORIGINS += [
'http://localhost:3000',
'http://127.0.0.1:3000',
]
Cause 5: Incorrect Form Method
Using POST but configured for GET, or vice versa.
# views.py
from django.views.decorators.http import require_http_methods
@require_http_methods(["GET"]) # Only allows GET
def my_view(request):
# But template has POST form
return render(request, 'form.html')
Fix: Ensure method consistency
# CORRECT
@require_http_methods(["GET", "POST"])
def my_view(request):
if request.method == "POST":
# Handle POST with CSRF
form = MyForm(request.POST)
if form.is_valid():
form.save()
else:
# Handle GET
form = MyForm()
return render(request, 'form.html', \{'form': form\})
Cause 6: Using @csrf_exempt Incorrectly
Exempting views from CSRF protection unnecessarily.
from django.views.decorators.csrf import csrf_exempt
# WRONG: Using csrf_exempt on a form view
@csrf_exempt
def submit_form(request):
# This disables CSRF protection!
return render(request, 'form.html')
When to use @csrf_exempt:
- API endpoints that use token authentication
- Webhooks from external services
- Views that don't modify state (though GET should be used)
Fix: Only exempt when absolutely necessary
# CORRECT: Only exempt API endpoints with proper auth
from rest_framework.decorators import api_view, authentication_classes
from rest_framework.authentication import TokenAuthentication
@api_view(['POST'])
@authentication_classes([TokenAuthentication])
def api_endpoint(request):
# Token auth provides security; CSRF not needed
return JsonResponse(\{'status': 'ok'\})
Cause 7: SameSite Cookie Settings
Modern browsers enforce SameSite cookie policies that can interfere with CSRF tokens.
# settings.py
# Problem: Cookies not sent with cross-origin requests
CSRF_COOKIE_SAMESITE = 'Strict' # Too restrictive
Fix: Adjust SameSite settings
# CORRECT: Balance security and functionality
CSRF_COOKIE_SAMESITE = 'Lax' # Default, works for most cases
# For cross-origin scenarios (be careful!)
CSRF_COOKIE_SAMESITE = 'None'
CSRF_COOKIE_SECURE = True # Required with SameSite=None
Special Cases
Django REST Framework
DRF handles CSRF differently depending on authentication.
# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
class MyAPIView(APIView):
# If using Session Authentication, CSRF is enforced
authentication_classes = [SessionAuthentication]
def post(self, request):
return Response(\{'status': 'ok'\})
Fix: Include CSRF token or use token auth
# Option 1: Use Token Authentication (no CSRF needed)
from rest_framework.authentication import TokenAuthentication
class MyAPIView(APIView):
authentication_classes = [TokenAuthentication]
# CSRF not required with token auth
# Option 2: Include CSRF token in headers (if using SessionAuth)
# See AJAX examples above
Testing Views with CSRF
Tests fail because CSRF checks are enabled.
from django.test import TestCase, Client
class MyViewTests(TestCase):
def test_submit_form(self):
client = Client()
response = client.post('/submit/', \{'data': 'value'\})
# This might fail with CSRF error
Fix: Django test client handles CSRF automatically
# CORRECT: Test client automatically includes CSRF token
from django.test import TestCase
class MyViewTests(TestCase):
def test_submit_form(self):
# Django's test client enforces CSRF by default
response = self.client.post('/submit/', \{'data': 'value'\})
self.assertEqual(response.status_code, 200)
# If you need to test CSRF failures:
def test_csrf_failure(self):
from django.test import Client as BaseClient
client = BaseClient(enforce_csrf_checks=True)
response = client.post('/submit/', \{'data': 'value'\})
self.assertEqual(response.status_code, 403)
iFrame and Embedded Forms
Forms in iframes from different domains can have CSRF issues.
<!-- Parent site: example.com -->
<iframe src="https://other-domain.com/form/"></iframe>
Fix: Configure CSRF for cross-origin
# settings.py for other-domain.com
# Allow embedding from specific domains
X_FRAME_OPTIONS = 'ALLOW-FROM https://example.com'
# Or use Content Security Policy
CSP_FRAME_ANCESTORS = ['https://example.com']
# Adjust CSRF cookie settings
CSRF_COOKIE_SAMESITE = 'None'
CSRF_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = ['https://example.com']
Best Practices
1. Always Include CSRF Tokens in Forms
<!-- Every POST form should have this -->
<form method="post">
{% csrf_token %}
<!-- form fields -->
</form>
2. Use Django Forms (Automatic CSRF)
# views.py
from django.shortcuts import render
from .forms import MyForm
def my_view(request):
form = MyForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
form.save()
return render(request, 'form.html', \{'form': form\})
<!-- template.html -->
<form method="post">
\{% csrf_token %\}
\{\{ form.as_p \}\}
<button type="submit">Submit</button>
</form>
3. Configure AJAX Globally
// Put this in your main JS file
document.addEventListener('DOMContentLoaded', function() {
const csrftoken = getCookie('csrftoken');
// Set default for all fetch requests
const originalFetch = window.fetch;
window.fetch = function(url, options = \{\}) \{
if (options.method && options.method.toUpperCase() !== 'GET') \{
options.headers = options.headers || \{\};
options.headers['X-CSRFToken'] = csrftoken;
\}
return originalFetch(url, options);
\};
\});
4. Never Disable CSRF Without Good Reason
# AVOID unless absolutely necessary
@csrf_exempt
def my_view(request):
pass
# INSTEAD, fix the CSRF configuration
Debugging Checklist
When you encounter CSRF errors, check:
- Is
{% csrf_token %}in your form template? - Is
CsrfViewMiddlewareinMIDDLEWAREsettings? - Are you sending POST data correctly?
- For AJAX: Is
X-CSRFTokenheader included? - Are cookies enabled in your browser?
- Are domains/origins configured correctly?
- Is HTTPS configuration correct (if using)?
- Are you testing with Django test client (auto-handles CSRF)?
Summary
CSRF protection is Django's defense against cross-site request forgery attacks. The error means Django didn't receive or couldn't validate the CSRF token.
Most common causes:
- Missing
{% csrf_token %}in forms - AJAX requests without CSRF header
- Domain/HTTPS misconfigurations
- Disabled CSRF middleware
Quick fixes:
- ✅ Add
{% csrf_token %}to all POST forms - ✅ Include CSRF token in AJAX request headers
- ✅ Configure
CSRF_TRUSTED_ORIGINSfor your domains - ✅ Ensure
CsrfViewMiddlewareis enabled - ✅ Never use
@csrf_exemptunless absolutely necessary
Remember: CSRF protection is a critical security feature. Don't disable it just to make errors go away - fix the underlying configuration instead!
Comments
Sign in to join the conversation