Skip to main content

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:

  1. User logs into your banking website
  2. In another tab, user visits a malicious site
  3. The malicious site contains code that submits a form to your banking site (like a money transfer)
  4. 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:

bash
pip install Flask-WTF

Step 2: Configure CSRF Protection

To enable CSRF protection, you need to configure a secret key for your Flask application:

python
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:

python
@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:

html
<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:

python
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):

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:

html
<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:

html
<meta name="csrf-token" content="{{ csrf_token() }}">

And in your Flask route:

python
@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):

python
@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:

python
@app.errorhandler(CSRFError)
def handle_csrf_error(e):
return render_template('csrf_error.html', reason=e.description), 400

In your csrf_error.html template:

html
<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:

python
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):

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:

  1. Understanding CSRF: How malicious sites can trick users into performing unwanted actions on your site
  2. Setting up CSRF protection: Using Flask-WTF to enable CSRF protection
  3. Including CSRF tokens in forms: Both for HTML forms and Flask-WTF forms
  4. AJAX requests: How to include CSRF tokens in AJAX requests
  5. Exempting routes: When and how to exempt routes from CSRF protection (with caution)
  6. Handling CSRF errors: Customizing error responses when CSRF validation fails
  7. 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

  1. Create a simple contact form with CSRF protection using Flask-WTF
  2. Implement a login form with CSRF protection and password validation
  3. Build an AJAX-powered comment submission system with proper CSRF handling
  4. Create a form that updates multiple related models while maintaining CSRF protection
  5. 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! :)