security
beginner
CSRF Protection in Flask Forms
min read
Frederick Tubiermont
CSRF Protection in Flask Forms
CSRF (Cross-Site Request Forgery) tricks a user's browser into submitting a form to your app from a different website. The fix is simple: include a secret token in every form that only your server knows.
What a CSRF Attack Looks Like
- User logs into your bank at
bank.com - User visits malicious
evil.com evil.comhas a hidden form that POSTs tobank.com/transfer- The user's browser sends their
bank.comsession cookie automatically - The bank processes the transfer — the user never clicked anything
The defense: require a token in the form that evil.com cannot know.
The Simple Approach: Generate a Token Per Session
# utils/csrf.py
import secrets
from flask import session
def generate_csrf_token():
"""Generate a CSRF token and store it in the session."""
if "csrf_token" not in session:
session["csrf_token"] = secrets.token_hex(32)
return session["csrf_token"]
def validate_csrf_token(token):
"""Check submitted token against the session token."""
session_token = session.get("csrf_token")
if not session_token or not token:
return False
# Use secrets.compare_digest to prevent timing attacks
return secrets.compare_digest(session_token, token)
Add the Token to Every Form
Make generate_csrf_token available in all templates:
# app.py
from utils.csrf import generate_csrf_token
app.jinja_env.globals["csrf_token"] = generate_csrf_token
Now in every form:
<!-- templates/any-form.html -->
<form method="POST" action="/submit">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="text" name="name" placeholder="Your name">
<button type="submit">Submit</button>
</form>
Validate on Every POST Route
# utils/csrf.py
from functools import wraps
from flask import request, abort
from utils.csrf import validate_csrf_token
def csrf_protect(f):
"""Decorator to validate CSRF token on POST requests."""
@wraps(f)
def decorated(*args, **kwargs):
if request.method == "POST":
token = request.form.get("csrf_token", "")
if not validate_csrf_token(token):
abort(403) # Forbidden
return f(*args, **kwargs)
return decorated
# In your routes
from utils.csrf import csrf_protect
@app.route("/profile/update", methods=["GET", "POST"])
@login_required
@csrf_protect
def update_profile():
if request.method == "POST":
name = request.form.get("name")
# Safe — CSRF token already validated
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"UPDATE users SET name = %s WHERE id = %s",
(name, session["user_id"])
)
conn.commit()
return redirect("/profile")
return render_template("profile.html")
CSRF for AJAX Requests
For fetch() calls from JavaScript, include the token in a header:
<!-- Make token available to JS -->
<meta name="csrf-token" content="{{ csrf_token() }}">
// Read token from meta tag
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
// Include in every POST fetch
async function postData(url, data) {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken
},
body: JSON.stringify(data)
});
return response.json();
}
# Validate header in API routes
def csrf_protect_ajax(f):
@wraps(f)
def decorated(*args, **kwargs):
if request.method == "POST":
token = request.headers.get("X-CSRF-Token", "")
if not validate_csrf_token(token):
return {"error": "Invalid CSRF token"}, 403
return f(*args, **kwargs)
return decorated
When You Don't Need CSRF Protection
- GET requests: Never use GET for actions that change data
- API endpoints with Authorization headers: These can't be forged by cross-site forms
- JSON-only APIs: If you check
Content-Type: application/json, browsers can't forge this from forms
Flask-WTF: The Automatic Alternative
If you use Flask-WTF for forms, CSRF protection is built in:
from flask_wtf import FlaskForm
from wtforms import StringField
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app) # Protects ALL forms automatically
The manual approach above is better for understanding what's happening. Flask-WTF is better when you have many forms and want it handled automatically.
Was this helpful?
Get More Flask Vibe Tutorials
Join 1,000+ developers getting weekly Flask tips and AI-friendly code patterns.
No spam. Unsubscribe anytime.