Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they're currently authenticated. Django provides robust built-in CSRF protection that's enabled by default.
<!-- Malicious website example -->
<!DOCTYPE html>
<html>
<head>
<title>Innocent Looking Page</title>
</head>
<body>
<h1>Check out this funny cat video!</h1>
<!-- Hidden malicious form that submits to your Django app -->
<form id="malicious-form" action="https://yourapp.com/transfer-money/" method="POST" style="display: none;">
<input type="hidden" name="amount" value="1000">
<input type="hidden" name="to_account" value="attacker-account">
</form>
<script>
// Automatically submit the form when page loads
document.getElementById('malicious-form').submit();
</script>
<!-- User sees this innocent content -->
<img src="cat-video-thumbnail.jpg" alt="Funny cat">
</body>
</html>
# Vulnerable view without CSRF protection
def transfer_money(request):
"""VULNERABLE: No CSRF protection"""
if request.method == 'POST':
amount = request.POST.get('amount')
to_account = request.POST.get('to_account')
# This could be executed by a CSRF attack!
user_account = request.user.account
user_account.transfer(amount, to_account)
return HttpResponse("Transfer completed")
return render(request, 'transfer_form.html')
# Other vulnerable scenarios:
# - Changing user email/password
# - Deleting user data
# - Making purchases
# - Posting content on behalf of user
# - Changing user preferences/settings
# Django's CSRF protection mechanism:
# 1. CSRF middleware generates a secret token
# 2. Token is stored in user's session
# 3. Token is embedded in forms via {% csrf_token %}
# 4. On form submission, Django validates the token
# 5. Request is rejected if token is missing or invalid
# settings.py - CSRF middleware (enabled by default)
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware', # CSRF protection
# ... other middleware
]
# CSRF settings
CSRF_COOKIE_AGE = 31449600 # 1 year
CSRF_COOKIE_DOMAIN = None
CSRF_COOKIE_HTTPONLY = False # Must be False for JavaScript access
CSRF_COOKIE_NAME = 'csrftoken'
CSRF_COOKIE_PATH = '/'
CSRF_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SECURE = True # True in production with HTTPS
CSRF_FAILURE_VIEW = 'django.views.csrf.csrf_failure'
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
CSRF_TRUSTED_ORIGINS = []
CSRF_USE_SESSIONS = False
<!-- forms.html - Basic CSRF protection -->
<form method="post" action="{% url 'transfer_money' %}">
{% csrf_token %} <!-- This adds the CSRF token -->
<div class="form-group">
<label for="amount">Amount:</label>
<input type="number" id="amount" name="amount" required>
</div>
<div class="form-group">
<label for="to_account">To Account:</label>
<input type="text" id="to_account" name="to_account" required>
</div>
<button type="submit">Transfer Money</button>
</form>
<!-- The {% csrf_token %} generates something like: -->
<input type="hidden" name="csrfmiddlewaretoken" value="abc123def456...">
# views.py - Properly protected views
from django.views.decorators.csrf import csrf_protect
from django.contrib.auth.decorators import login_required
@login_required
@csrf_protect # Explicitly require CSRF protection
def transfer_money(request):
"""Secure money transfer with CSRF protection"""
if request.method == 'POST':
form = MoneyTransferForm(request.POST)
if form.is_valid():
# Additional security checks
amount = form.cleaned_data['amount']
to_account = form.cleaned_data['to_account']
# Verify user has sufficient funds
if request.user.account.balance < amount:
messages.error(request, "Insufficient funds")
return render(request, 'transfer_form.html', {'form': form})
# Verify destination account exists
try:
destination = Account.objects.get(number=to_account)
except Account.DoesNotExist:
messages.error(request, "Invalid destination account")
return render(request, 'transfer_form.html', {'form': form})
# Perform transfer
try:
request.user.account.transfer(amount, destination)
messages.success(request, f"Successfully transferred ${amount}")
# Log the transaction for audit
logger.info(f"Money transfer: {request.user.username} -> {to_account}, Amount: ${amount}")
return redirect('account_dashboard')
except TransferError as e:
messages.error(request, f"Transfer failed: {str(e)}")
else:
messages.error(request, "Please correct the errors below")
else:
form = MoneyTransferForm()
return render(request, 'transfer_form.html', {'form': form})
# Class-based view with CSRF protection
from django.views.generic import FormView
from django.contrib.auth.mixins import LoginRequiredMixin
class MoneyTransferView(LoginRequiredMixin, FormView):
"""Secure money transfer view"""
template_name = 'transfer_form.html'
form_class = MoneyTransferForm
success_url = '/account/dashboard/'
def form_valid(self, form):
"""Process valid form with additional security checks"""
amount = form.cleaned_data['amount']
to_account = form.cleaned_data['to_account']
# Security validations
if not self.validate_transfer(amount, to_account):
return self.form_invalid(form)
# Perform transfer
try:
self.request.user.account.transfer(amount, to_account)
messages.success(self.request, f"Successfully transferred ${amount}")
return super().form_valid(form)
except TransferError as e:
form.add_error(None, f"Transfer failed: {str(e)}")
return self.form_invalid(form)
def validate_transfer(self, amount, to_account):
"""Additional transfer validation"""
# Check daily transfer limit
daily_total = self.request.user.account.get_daily_transfer_total()
if daily_total + amount > 5000: # $5000 daily limit
messages.error(self.request, "Daily transfer limit exceeded")
return False
return True
// static/js/csrf.js - CSRF token handling for AJAX
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;
}
// Get CSRF token
const csrftoken = getCookie('csrftoken');
// Method 1: Include CSRF token in AJAX headers
function csrfSafeMethod(method) {
// These HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
// Method 2: Include CSRF token in form data
function transferMoney(amount, toAccount) {
$.ajax({
url: '/transfer-money/',
type: 'POST',
data: {
'amount': amount,
'to_account': toAccount,
'csrfmiddlewaretoken': csrftoken // Include token in data
},
success: function(response) {
alert('Transfer successful!');
},
error: function(xhr, status, error) {
alert('Transfer failed: ' + error);
}
});
}
// Method 3: Using fetch API with CSRF token
async function transferMoneyFetch(amount, toAccount) {
try {
const response = await fetch('/transfer-money/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': csrftoken,
},
body: new URLSearchParams({
'amount': amount,
'to_account': toAccount,
})
});
if (response.ok) {
const result = await response.json();
alert('Transfer successful!');
} else {
throw new Error('Transfer failed');
}
} catch (error) {
alert('Transfer failed: ' + error.message);
}
}
<!-- Include CSRF token for JavaScript use -->
<script>
// Method 1: Inline script with CSRF token
window.csrfToken = '{{ csrf_token }}';
</script>
<!-- Method 2: Meta tag approach -->
<meta name="csrf-token" content="{{ csrf_token }}">
<script>
// Get CSRF token from meta tag
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
</script>
<!-- Method 3: Hidden input approach -->
<div id="csrf-token" data-token="{{ csrf_token }}" style="display: none;"></div>
<script>
// Get CSRF token from data attribute
const csrfToken = document.getElementById('csrf-token').dataset.token;
</script>
# views.py - Custom CSRF failure view
def csrf_failure(request, reason=""):
"""Custom CSRF failure handler"""
# Log CSRF failure for security monitoring
logger.warning(f"CSRF failure: {reason}", extra={
'ip_address': get_client_ip(request),
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
'path': request.path,
'user': getattr(request, 'user', None),
'reason': reason,
})
# Different responses based on request type
if request.headers.get('Content-Type') == 'application/json':
return JsonResponse({
'error': 'CSRF verification failed',
'message': 'Please refresh the page and try again'
}, status=403)
# Render custom CSRF error page
context = {
'reason': reason,
'support_email': settings.SUPPORT_EMAIL,
}
return render(request, 'csrf_failure.html', context, status=403)
# settings.py
CSRF_FAILURE_VIEW = 'myapp.views.csrf_failure'
# settings.py - Configure trusted origins for cross-origin requests
CSRF_TRUSTED_ORIGINS = [
'https://api.yourdomain.com',
'https://mobile.yourdomain.com',
'https://partner.example.com',
]
# For development with different ports
if DEBUG:
CSRF_TRUSTED_ORIGINS.extend([
'http://localhost:3000', # React dev server
'http://127.0.0.1:3000',
'http://localhost:8080', # Vue dev server
])
# middleware.py - Enhanced CSRF middleware
import time
from django.middleware.csrf import CsrfViewMiddleware
from django.core.cache import cache
class EnhancedCsrfMiddleware(CsrfViewMiddleware):
"""Enhanced CSRF middleware with additional security features"""
def process_request(self, request):
"""Enhanced request processing with rate limiting"""
# Rate limit CSRF failures per IP
client_ip = self.get_client_ip(request)
failure_key = f"csrf_failures:{client_ip}"
failure_count = cache.get(failure_key, 0)
if failure_count >= 10: # Max 10 failures per hour
logger.warning(f"CSRF failure rate limit exceeded for IP: {client_ip}")
return HttpResponseTooManyRequests("Too many CSRF failures")
return super().process_request(request)
def process_view(self, request, callback, callback_args, callback_kwargs):
"""Enhanced view processing with additional validation"""
# Check for suspicious patterns in CSRF failures
if hasattr(request, '_csrf_processing_done'):
return None
# Additional CSRF validation for sensitive operations
if self.is_sensitive_operation(request):
if not self.validate_additional_csrf_checks(request):
self.record_csrf_failure(request, "Additional validation failed")
return self.csrf_failure(request, "Additional validation required")
return super().process_view(request, callback, callback_args, callback_kwargs)
def is_sensitive_operation(self, request):
"""Check if request is for sensitive operation"""
sensitive_paths = [
'/transfer-money/',
'/change-password/',
'/delete-account/',
'/admin/',
]
return any(request.path.startswith(path) for path in sensitive_paths)
def validate_additional_csrf_checks(self, request):
"""Additional CSRF validation for sensitive operations"""
# Check request timing (prevent replay attacks)
csrf_time = request.POST.get('csrf_timestamp')
if csrf_time:
try:
timestamp = float(csrf_time)
if time.time() - timestamp > 300: # 5 minutes max
return False
except (ValueError, TypeError):
return False
# Check referrer for additional validation
referer = request.META.get('HTTP_REFERER', '')
if not referer.startswith(f"https://{request.get_host()}"):
return False
return True
def record_csrf_failure(self, request, reason):
"""Record CSRF failure for monitoring"""
client_ip = self.get_client_ip(request)
failure_key = f"csrf_failures:{client_ip}"
# Increment failure count
failure_count = cache.get(failure_key, 0) + 1
cache.set(failure_key, failure_count, 3600) # 1 hour
# Log failure
logger.warning(f"CSRF failure: {reason}", extra={
'ip_address': client_ip,
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
'path': request.path,
'failure_count': failure_count,
})
def get_client_ip(self, request):
"""Get client IP address"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
# views.py - CSRF exemptions (use carefully!)
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
# API endpoints that use other authentication methods
@csrf_exempt
def api_webhook(request):
"""Webhook endpoint with alternative authentication"""
# Verify webhook signature instead of CSRF
signature = request.META.get('HTTP_X_WEBHOOK_SIGNATURE')
if not verify_webhook_signature(request.body, signature):
return HttpResponseForbidden("Invalid signature")
# Process webhook
data = json.loads(request.body)
process_webhook_data(data)
return JsonResponse({'status': 'success'})
# Class-based view exemption
@method_decorator(csrf_exempt, name='dispatch')
class APIWebhookView(View):
"""API webhook with custom authentication"""
def post(self, request):
# Custom authentication logic
if not self.authenticate_api_request(request):
return HttpResponseForbidden("Authentication failed")
# Process request
return JsonResponse({'status': 'received'})
def authenticate_api_request(self, request):
"""Custom API authentication"""
api_key = request.META.get('HTTP_X_API_KEY')
return api_key and verify_api_key(api_key)
# Partial CSRF exemption for specific methods
from django.views.decorators.csrf import requires_csrf_token
@requires_csrf_token
def mixed_endpoint(request):
"""Endpoint that requires CSRF for some methods but not others"""
if request.method == 'GET':
# GET requests don't need CSRF protection
return render(request, 'form.html')
elif request.method == 'POST':
# POST requests are automatically protected by CSRF middleware
# Process form submission
pass
# API views with token authentication instead of CSRF
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
@api_view(['POST'])
@authentication_classes([TokenAuthentication])
@permission_classes([IsAuthenticated])
def api_transfer_money(request):
"""API endpoint with token authentication (no CSRF needed)"""
serializer = MoneyTransferSerializer(data=request.data)
if serializer.is_valid():
# Additional API-specific validation
if not validate_api_transfer_limits(request.user, serializer.validated_data):
return Response({'error': 'Transfer limits exceeded'}, status=400)
# Process transfer
try:
result = process_money_transfer(
user=request.user,
**serializer.validated_data
)
return Response({'status': 'success', 'transaction_id': result.id})
except TransferError as e:
return Response({'error': str(e)}, status=400)
return Response(serializer.errors, status=400)
# Custom authentication middleware for APIs
class APIAuthenticationMiddleware:
"""Custom API authentication that bypasses CSRF for authenticated API requests"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Check if this is an API request with valid token
if request.path.startswith('/api/'):
api_token = request.META.get('HTTP_AUTHORIZATION')
if api_token and api_token.startswith('Token '):
token = api_token[6:] # Remove 'Token ' prefix
user = self.authenticate_token(token)
if user:
request.user = user
# Mark request as API authenticated (bypass CSRF)
request._dont_enforce_csrf_checks = True
response = self.get_response(request)
return response
def authenticate_token(self, token):
"""Authenticate API token"""
try:
from rest_framework.authtoken.models import Token
token_obj = Token.objects.get(key=token)
return token_obj.user
except Token.DoesNotExist:
return None
# tests.py - Testing CSRF protection
from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.urls import reverse
class CSRFProtectionTests(TestCase):
"""Test CSRF protection functionality"""
def setUp(self):
self.client = Client(enforce_csrf_checks=True)
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
def test_csrf_protection_enabled(self):
"""Test that CSRF protection is enabled"""
self.client.login(username='testuser', password='testpass123')
# POST without CSRF token should fail
response = self.client.post(reverse('transfer_money'), {
'amount': 100,
'to_account': '12345'
})
self.assertEqual(response.status_code, 403)
def test_csrf_token_required(self):
"""Test that valid CSRF token allows request"""
self.client.login(username='testuser', password='testpass123')
# Get CSRF token
response = self.client.get(reverse('transfer_form'))
csrf_token = response.context['csrf_token']
# POST with valid CSRF token should succeed
response = self.client.post(reverse('transfer_money'), {
'amount': 100,
'to_account': '12345',
'csrfmiddlewaretoken': csrf_token
})
self.assertNotEqual(response.status_code, 403)
def test_ajax_csrf_protection(self):
"""Test CSRF protection for AJAX requests"""
self.client.login(username='testuser', password='testpass123')
# Get CSRF token
response = self.client.get(reverse('transfer_form'))
csrf_token = response.cookies['csrftoken'].value
# AJAX request with CSRF header
response = self.client.post(
reverse('api_transfer_money'),
{'amount': 100, 'to_account': '12345'},
HTTP_X_CSRFTOKEN=csrf_token,
content_type='application/json'
)
self.assertNotEqual(response.status_code, 403)
def test_csrf_exemption(self):
"""Test that exempted views don't require CSRF"""
# Webhook endpoint should not require CSRF
response = self.client.post(reverse('api_webhook'), {
'event': 'payment_received',
'amount': 100
}, HTTP_X_WEBHOOK_SIGNATURE='valid_signature')
# Should not return 403 (CSRF failure)
self.assertNotEqual(response.status_code, 403)
CsrfViewMiddleware enabled{% csrf_token %} in all formsCSRF_COOKIE_SECURE = True in productionCSRF_COOKIE_SAMESITE = 'Strict' for maximum securityCSRF_TRUSTED_ORIGINS carefullyNow that you understand CSRF protection, let's explore Cross-Site Scripting (XSS) prevention and how Django helps protect against these attacks.
Django Security Philosophy
Django's approach to security is built on the principle of "secure by default" - providing robust security features out of the box while making it easy for developers to build secure applications. This chapter explores Django's security philosophy and core principles.
Cross Site Scripting
Cross-Site Scripting (XSS) is a vulnerability that allows attackers to inject malicious scripts into web pages viewed by other users. Django provides robust protection against XSS attacks through automatic template escaping and security best practices.