Cross‑Site Request Forgery (CSRF) remains one of the most sneaky web vulnerabilities, and Python developers often wonder how to defend their applications without sacrificing performance or developer ergonomics. In this guide we’ll explore what CSRF is, why it matters, and how to implement rock‑solid protection in the most popular Python web frameworks—Django, Flask, and FastAPI. You’ll also learn the underlying principles, see code‑level examples, and walk away with a ready‑to‑use checklist that keeps your site safe and your SEO rankings high.
Understanding CSRF and Its Risks
What is CSRF?
CSRF exploits the trust that a web browser has in a logged‑in user. An attacker tricks the victim’s browser into sending a state‑changing request (such as POST /transfer) to a target site where the user is already authenticated. Because the browser automatically includes cookies, the request appears legitimate, and the server processes it as if the user had initiated the action.
Real‑world impact
- Financial loss: Unauthorized fund transfers, subscription cancellations, or purchase orders.
- Data integrity: Deleting or modifying user records, changing passwords, or altering settings.
- Reputation damage: Users lose trust when their accounts are compromised, which can hurt search rankings and organic traffic.
Core Principles of CSRF Protection
- Synchronizer token pattern: Generate a unique, unpredictable token per user session and require it on every state‑changing request.
- SameSite cookie attribute: Instruct browsers not to send cookies on cross‑origin requests (e.g.,
SameSite=LaxorStrict). - Double‑submit cookie: Send the token both as a cookie and as a request parameter, then compare them on the server.
- Origin/Referer validation: Verify that the
OriginorRefererheader matches your domain for non‑GET requests.
When these principles are combined, an attacker’s forged request will lack a valid token or will be blocked by the browser, effectively neutralizing the threat.
Implementing CSRF Protection in Popular Python Frameworks
Django – Built‑in CSRF Middleware
Django ships with a mature CSRF system that works out of the box. The CsrfViewMiddleware validates tokens for any POST, PUT, PATCH, or DELETE request.
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', # ← enabled by default
# …
]
# template.html
<form method="post" action="/submit/">
{% csrf_token %}
<input type="text" name="name">
<button type="submit">Send</button>
</form>
The {% csrf_token %} tag injects a hidden input containing the session‑specific token. Django also sets a csrftoken cookie, enabling the double‑submit pattern for AJAX calls:
fetch('/api/data/', {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'), // custom helper
'Content-Type': 'application/json'
},
body: JSON.stringify({key: 'value'})
});
Flask – Using Flask‑WTF or Manual Tokens
Flask does not include CSRF protection by default, but the Flask‑WTF extension adds a seamless layer.
# app.py
from flask import Flask, render_template, request
from flask_wtf import CSRFProtect
app = Flask(__name__)
app.config['SECRET_KEY'] = 'a-very-secret-key'
csrf = CSRFProtect(app) # ← activates protection for all POST routes
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# process login
pass
return render_template('login.html')
In your Jinja2 template, include the token with {{ csrf_token() }}:
<form method="post" action="{{ url_for('login') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="text" name="username">
<input type="password" name="password">
<button type="submit">Log In</button>
</form>
If you prefer a lightweight approach, you can generate and verify tokens manually:
import secrets
from flask import session, abort, request
def generate_csrf_token():
token = secrets.token_urlsafe(32)
session['csrf_token'] = token
return token
def validate_csrf():
token = session.get('csrf_token')
header = request.headers.get('X-CSRFToken') or request.form.get('csrf_token')
if not token or token != header:
abort(400, 'Invalid CSRF token')
FastAPI – Middleware for Token Validation
FastAPI, built on Starlette, encourages explicit security layers. Below is a minimal CSRF middleware that checks a token stored in a signed cookie.
# csrf_middleware.py
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
from itsdangerous import URLSafeSerializer, BadSignature
import os
serializer = URLSafeSerializer(os.getenv('SECRET_KEY', 'fallback-secret'))
class CSRFMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Only protect unsafe methods
if request.method not in ('GET', 'HEAD', 'OPTIONS'):
cookie = request.cookies.get('csrf_token')
header = request.headers.get('X-CSRF-Token')
if not cookie or not header:
return Response('CSRF token missing', status_code=400)
try:
token = serializer.loads(cookie)
if token != header:
raise ValueError('Token mismatch')
except (BadSignature, ValueError):
return Response('Invalid CSRF token', status_code=400)
response = await call_next(request)
# Set token for safe methods
if request.method == 'GET' and 'csrf_token' not in request.cookies:
token = serializer.dumps(secrets.token_urlsafe(32))
response.set_cookie('csrf_token', token, httponly=False, samesite='Lax')
return response
# main.py
from fastapi import FastAPI, Request
from csrf_middleware import CSRFMiddleware
app = FastAPI()
app.add_middleware(CSRFMiddleware)
@app.post('/comment')
async def add_comment(request: Request):
data = await request.json()
# process comment
return {'status': 'saved'}
Clients send the token via the X-CSRF-Token header, which the middleware validates against the signed cookie.
Manual CSRF Token Management (When Framework Support Is Not Enough)
Sometimes you need a custom solution—perhaps for a microservice that only serves JSON APIs. Follow these steps:
- Generate a cryptographically strong token. Use
secrets.token_urlsafe()and store it in the user’s session or a signed cookie. - Expose the token to the client. Render it in HTML, return it in a JSON payload, or set it as a cookie with
SameSite=Lax. - Require the token on every state‑changing request. Accept it via a custom header (e.g.,
X-CSRF-Token) or a hidden form field. - Validate the token server‑side. Compare the received value with the stored one; reject the request with
400 Bad Requeston mismatch. - Rotate the token periodically. Regenerate after login, logout, or every few hours to limit exposure.
Common Pitfalls and How to Avoid Them
- Disabling CSRF for JSON endpoints. Never blanket‑disable protection; instead, enforce token checks on
application/jsonrequests just like form posts. - Storing tokens in insecure cookies. Mark CSRF cookies as
HttpOnly=False(so JavaScript can read them) but always setSecureandSameSite=Laxin production. - Relying solely on
Leave a Reply