Security

Cross Site Request Forgery

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.

Cross Site Request Forgery

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.

Understanding CSRF Attacks

How CSRF Works

<!-- 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>

CSRF Attack Scenarios

# 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

How Django CSRF Protection Works

# 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

Basic CSRF Protection in Templates

<!-- 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...">

CSRF Protection in Views

# 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

AJAX and CSRF Protection

CSRF with JavaScript/AJAX

// 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);
    }
}

CSRF Token in Templates for JavaScript

<!-- 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>

Advanced CSRF Configuration

Custom CSRF Failure Handling

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

CSRF Trusted Origins

# 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
    ])

Custom CSRF Middleware

# 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

CSRF Exemptions and Special Cases

When to Exempt Views from CSRF

# 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 Authentication vs CSRF

# 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

Testing CSRF Protection

Unit Tests for CSRF

# 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)

Best Practices

CSRF Security Guidelines

  1. Always Use CSRF Protection
    • Keep CsrfViewMiddleware enabled
    • Use {% csrf_token %} in all forms
    • Include CSRF tokens in AJAX requests
  2. Secure CSRF Configuration
    • Set CSRF_COOKIE_SECURE = True in production
    • Use CSRF_COOKIE_SAMESITE = 'Strict' for maximum security
    • Configure CSRF_TRUSTED_ORIGINS carefully
  3. Handle CSRF Failures Gracefully
    • Provide user-friendly error messages
    • Log CSRF failures for security monitoring
    • Implement rate limiting for repeated failures
  4. API Considerations
    • Use proper authentication for APIs instead of CSRF exemption
    • Document when and why CSRF is exempted
    • Implement alternative security measures for exempted endpoints
  5. Testing and Monitoring
    • Test CSRF protection in automated tests
    • Monitor CSRF failure rates
    • Regular security audits of CSRF implementation

Next Steps

Now that you understand CSRF protection, let's explore Cross-Site Scripting (XSS) prevention and how Django helps protect against these attacks.