Flask CSRF Protection
Introduction
When building web applications with Flask, security should be a top priority. One of the common security vulnerabilities in web applications is Cross-Site Request Forgery (CSRF). CSRF attacks occur when a malicious website tricks users into submitting forms to your application while they're authenticated, potentially causing unwanted actions to be performed on their behalf.
Flask provides protection against CSRF attacks through its integration with the Flask-WTF extension. In this tutorial, we'll learn how to implement CSRF protection in Flask applications to secure your forms and protect your users.
What is CSRF?
Before diving into the implementation, let's understand what CSRF is:
Cross-Site Request Forgery is an attack that forces authenticated users to execute unwanted actions on a web application they're currently logged into. For example:
- User logs into your banking website
- In another tab, user visits a malicious site
- The malicious site contains code that submits a form to your banking site (like a money transfer)
- Since the user is authenticated, the browser sends their cookies, and the banking site processes the request
CSRF protection prevents these attacks by ensuring that form submissions include a special token that only your legitimate site can generate.
Setting Up CSRF Protection
Step 1: Install Flask-WTF
Flask's CSRF protection is provided through the Flask-WTF extension. Let's start by installing it:
pip install Flask-WTF
Step 2: Configure CSRF Protection
To enable CSRF protection, you need to configure a secret key for your Flask application:
from flask import Flask, render_template, request, redirect
from flask_wtf.csrf import CSRFProtect
app = Flask(__name__)
# Set a secret key for CSRF protection
app.config['SECRET_KEY'] = 'a-very-secret-key-should-be-stored-securely'
# Initialize CSRF protection
csrf = CSRFProtect(app)
# Your routes will go here
The SECRET_KEY
should be a complex, randomly generated string in production applications and should be stored securely (e.g., through environment variables).
Using CSRF Protection with Forms
Basic HTML Forms
To protect a regular HTML form, you need to include a CSRF token in your form:
@app.route('/basic-form', methods=['GET', 'POST'])
def basic_form():
if request.method == 'POST':
# Process form data
name = request.form.get('name')
return f"Form submitted! Hello, {name}!"
return render_template('basic_form.html')
In your template (basic_form.html
), include the CSRF token:
<form method="POST" action="/basic-form">
<!-- CSRF token -->
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<label for="name">Your Name:</label>
<input type="text" id="name" name="name" required>
<button type="submit">Submit</button>
</form>
Using Flask-WTF Forms
If you're using Flask-WTF forms, CSRF protection is automatically included:
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class NameForm(FlaskForm):
name = StringField('Your Name', validators=[DataRequired()])
submit = SubmitField('Submit')
@app.route('/wtf-form', methods=['GET', 'POST'])
def wtf_form():
form = NameForm()
if form.validate_on_submit():
# Form is valid and includes a valid CSRF token
return f"Form submitted! Hello, {form.name.data}!"
return render_template('wtf_form.html', form=form)
In your template (wtf_form.html
):
<form method="POST">
<!-- The form.csrf_token is automatically included with Flask-WTF -->
{{ form.csrf_token }}
<div>
{{ form.name.label }}
{{ form.name }}
</div>
{{ form.submit }}
</form>
CSRF Protection for AJAX Requests
For AJAX requests, you'll need to include the CSRF token in your headers:
<script>
document.addEventListener('DOMContentLoaded', function() {
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
document.getElementById('ajax-form').addEventListener('submit', function(e) {
e.preventDefault();
fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
name: document.getElementById('ajax-name').value
})
})
.then(response => response.json())
.then(data => {
console.log(data);
document.getElementById('result').textContent = data.message;
});
});
});
</script>
In your HTML head section:
<meta name="csrf-token" content="{{ csrf_token() }}">
And in your Flask route:
@app.route('/api/submit', methods=['POST'])
def api_submit():
data = request.json
return {'success': True, 'message': f"Hello, {data.get('name')}!"}
CSRF Protection Exemption
In some cases, you might want to exempt specific routes from CSRF protection (like for third-party webhooks):
@app.route('/webhook', methods=['POST'])
@csrf.exempt
def webhook():
# Process webhook data without CSRF validation
return "Webhook received"
Note: Be very careful when exempting routes from CSRF protection. Only do this for routes that genuinely need to be accessed from external sites and implement alternative security measures.
Handling CSRF Errors
When CSRF validation fails, Flask returns a 400 Bad Request error. You can customize the behavior:
@app.errorhandler(CSRFError)
def handle_csrf_error(e):
return render_template('csrf_error.html', reason=e.description), 400
In your csrf_error.html
template:
<h1>CSRF Validation Failed</h1>
<p>{{ reason }}</p>
<p>Please go back and try again.</p>
Real-world Example: User Profile Update Form
Let's implement a complete example of a user profile update form with CSRF protection:
from flask import Flask, render_template, request, redirect, flash, url_for
from flask_wtf import FlaskForm, CSRFProtect
from wtforms import StringField, EmailField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secure-secret-key'
csrf = CSRFProtect(app)
# Mock user data
user = {
'username': 'demo_user',
'email': '[email protected]',
'bio': 'I love Flask!'
}
class ProfileForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=20)])
email = EmailField('Email', validators=[DataRequired(), Email()])
bio = StringField('Bio')
submit = SubmitField('Update Profile')
@app.route('/profile', methods=['GET', 'POST'])
def edit_profile():
form = ProfileForm(obj=user)
if form.validate_on_submit():
# Update user data
user['username'] = form.username.data
user['email'] = form.email.data
user['bio'] = form.bio.data
flash('Profile updated successfully!', 'success')
return redirect(url_for('edit_profile'))
return render_template('profile.html', form=form, user=user)
if __name__ == '__main__':
app.run(debug=True)
Profile template (profile.html
):
<!DOCTYPE html>
<html>
<head>
<title>Edit Profile</title>
<style>
.flash-message { padding: 10px; background-color: #dff0d8; margin-bottom: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
</style>
</head>
<body>
<h1>Edit Profile</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash-message">
{{ messages[0] }}
</div>
{% endif %}
{% endwith %}
<form method="POST">
{{ form.csrf_token }}
<div class="form-group">
{{ form.username.label }}
{{ form.username(class="form-control") }}
{% if form.username.errors %}
<div class="error">{{ form.username.errors[0] }}</div>
{% endif %}
</div>
<div class="form-group">
{{ form.email.label }}
{{ form.email(class="form-control") }}
{% if form.email.errors %}
<div class="error">{{ form.email.errors[0] }}</div>
{% endif %}
</div>
<div class="form-group">
{{ form.bio.label }}
{{ form.bio(class="form-control") }}
</div>
{{ form.submit(class="btn") }}
</form>
<div style="margin-top: 20px;">
<h2>Current Profile</h2>
<p><strong>Username:</strong> {{ user.username }}</p>
<p><strong>Email:</strong> {{ user.email }}</p>
<p><strong>Bio:</strong> {{ user.bio }}</p>
</div>
</body>
</html>
Summary
CSRF protection is a critical security feature that helps protect your Flask application and users from cross-site request forgery attacks. Here's what we covered:
- Understanding CSRF: How malicious sites can trick users into performing unwanted actions on your site
- Setting up CSRF protection: Using Flask-WTF to enable CSRF protection
- Including CSRF tokens in forms: Both for HTML forms and Flask-WTF forms
- AJAX requests: How to include CSRF tokens in AJAX requests
- Exempting routes: When and how to exempt routes from CSRF protection (with caution)
- Handling CSRF errors: Customizing error responses when CSRF validation fails
- Real-world example: A complete profile update form with CSRF protection
By implementing CSRF protection in your Flask applications, you add an essential layer of security that protects your users from potential attacks.
Additional Resources
Exercises
- Create a simple contact form with CSRF protection using Flask-WTF
- Implement a login form with CSRF protection and password validation
- Build an AJAX-powered comment submission system with proper CSRF handling
- Create a form that updates multiple related models while maintaining CSRF protection
- Research and implement other security measures that complement CSRF protection in Flask
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)