Skip to main content

Overview

Cross-Site Request Forgery (CSRF) is an attack that tricks a victim’s browser into submitting an unintended request to an application where they are already authenticated. This can lead to unauthorized actions like changing a password, making a purchase, or deleting an account, all without the user’s knowledge. The attack works because the browser automatically sends authentication cookies with the request.

Business Impact

CSRF can lead to unauthorized financial transactions, data modification, or full account takeover (if used to change a user’s email or password). It erodes user trust and can cause significant financial and reputational damage.

Reference Details

CWE ID: CWE-352 OWASP Top 10 (2021): A01:2021 - Broken Access Control Severity: High

Framework-Specific Analysis and Remediation

Modern web frameworks (Rails, Django, Laravel, Spring Security, ASP.NET) have built-in CSRF protection enabled by default for session-based authentication. The vulnerability is not that the framework is weak, but that a developer disables this protection, often for convenience (e.g., for an API) or by mistake. The fix is to re-enable and correctly use the framework’s anti-CSRF token mechanism.
  • Python
  • Java
  • .NET(C#)
  • PHP
  • Node.js
  • Ruby

Framework Context

Django’s CsrfViewMiddleware is enabled by default. It requires a {% csrf_token %} in all POST forms. The vulnerability is using the @csrf_exempt decorator on a view.

Vulnerable Scenario 1: Exempting a Function-Based View

A view that changes a user’s email is explicitly exempted from CSRF checks for convenience.
# users/views.py
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required

@login_required
@csrf_exempt # DANGEROUS
def update_email(request):
    if request.method == 'POST':
        # This action can be triggered from a malicious site
        request.user.email = request.POST.get('email')
        request.user.save()
    return render(request, 'profile.html')

Vulnerable Scenario 2: Exempting a Class-Based View

A FormView for deleting an account is exempted using @method_decorator.
# users/views.py
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import FormView

@method_decorator(csrf_exempt, name='dispatch') # DANGEROUS
class DeleteAccountView(FormView):
    # ... form_class and success_url ...
    
    def form_valid(self, form):
        # This deletion can be triggered by a malicious POST
        self.request.user.delete()
        return super().form_valid(form)

Mitigation and Best Practices

Remove the @csrf_exempt and @method_decorator(csrf_exempt) decorators. Ensure your POST form in the template includes the {% csrf_token %} tag. For AJAX, pass the token in a custom X-CSRFToken header.

Secure Code Example

# users/views.py (Secure Version)
@login_required
# @csrf_exempt decorator is REMOVED
def update_email(request):
    if request.method == 'POST':
        request.user.email = request.POST.get('email')
        request.user.save()
    return render(request, 'profile.html')

# templates/profile.html (Secure Version)
/*
<form method="post">
    {% csrf_token %} <label>Email:</label>
    <input type="email" name="email">
    <button type="submit">Update</button>
</form>
*/

Testing Strategy

Write an integration test that performs a POST request to the endpoint without the CSRF token. The test should assert that the response is a 403 Forbidden.
# users/tests.py
def test_update_email_fails_without_csrf_token(self):
    # self.client is logged in
    response = self.client.post(reverse('update-email'), {
        'email': 'new@example.com'
    })
    
    # A secure endpoint will return 403 Forbidden
    self.assertEqual(response.status_code, 403)