Django Security

CSRF Protection in Django: What It Actually Does

Almost every Django developer has typed {% csrf_token %} without stopping to ask what it's actually defending against, or what @csrf_exempt quietly removes.

F Fora AI Team July 2, 2026 6 min read

Cross-Site Request Forgery is one of those vulnerabilities that's easy to forget about precisely because Django handles it well by default. The middleware is on, the template tag is one line, and most developers never think about it again — until a form breaks in an unfamiliar way, someone Googles the error, and the fix that comes back is @csrf_exempt.

The actual attack

Say you're logged into your bank's website in one tab. In another tab, you visit a malicious page. That page contains a form that auto-submits a POST request to yourbank.com/transfer with the attacker's account number as the destination. Your browser will happily include your bank's session cookie with that request, because cookies are sent automatically to their originating domain regardless of which page triggered the request. Without CSRF protection, your bank's server has no way to distinguish "the user submitted this form on our site" from "some other site tricked the user's browser into submitting it."

How the token fixes this

Django's CSRF protection adds a second, unpredictable value that has to be present in the request body — not just the cookie. Because the attacker's page doesn't have permission to read your bank's cookies or generate a valid matching token (same-origin policy prevents that), it can't reproduce this value, and the server rejects the forged request. That's what {% csrf_token %} renders into the form, and what CsrfViewMiddleware validates on the way in.

What @csrf_exempt actually removes

This decorator doesn't add an alternative protection — it removes the check entirely for that view. There are legitimate reasons to do this: a webhook endpoint that receives POST requests from a third-party service (like a payment processor) can't include a Django CSRF token, because it's not a form your own templates rendered.

The mistake is reaching for @csrf_exempt on a view that is reachable from a browser with a logged-in session, just because it was throwing an error during development. If you're exempting a webhook, verify the request some other way — almost every provider (Stripe, PayPal, GitHub) signs their webhook payloads with a secret you can check instead.

A quick self-check

If you see @csrf_exempt in a codebase, ask: does this endpoint only ever receive requests from a server-to-server integration with its own signature verification? If yes, it's probably fine. If it's a normal form a logged-in user submits from your own frontend, it shouldn't be there.

← Back to all posts