Flask CSRF Protection
Introduction
Cross-Site Request Forgery (CSRF) is a type of security vulnerability where attackers trick users into performing unwanted actions on a website where they're authenticated. For example, imagine you're logged into your bank's website in one browser tab, and you visit a malicious website in another tab. The malicious site could contain code that submits a form to your bank's "transfer money" endpoint without your knowledge.
Flask provides built-in CSRF protection through the Flask-WTF extension, which helps prevent these types of attacks by ensuring that form submissions come from your own website and not from malicious sources.
In this tutorial, you'll learn:
- What CSRF attacks are and why they're dangerous
- How to implement CSRF protection in Flask applications
- Best practices for handling CSRF tokens
- Testing your CSRF protection
Understanding CSRF Attacks
Before we dive into the implementation, let's understand how CSRF attacks work:
- A user logs into a legitimate website (e.g., a banking site)
- The website sets authentication cookies in the user's browser
- Without logging out, the user visits a malicious website
- The malicious site triggers a request to the legitimate site
- The browser automatically includes the authentication cookies with the request
- The legitimate site processes the request as if it came from the user
CSRF protection works by requiring a special token (the CSRF token) to be included with each form submission. This token is validated on the server side to ensure the request originated from your own website.
Installing Flask-WTF
Flask-WTF is an extension that integrates WTForms with Flask and provides CSRF protection. Let's start by installing it:
pip install Flask-WTF
Basic CSRF Protection Setup
Here's how to set up basic CSRF protection in a Flask application:
from flask import Flask, render_template, request, redirect, url_for
from flask_wtf.csrf import CSRFProtect
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key' # Replace with a strong secret key
csrf = CSRFProtect(app)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/submit', methods=['POST'])
def submit():
# Process form data
name = request.form.get('name')
return f"Form submitted successfully! Hello, {name}!"
if __name__ == '__main__':
app.run(debug=True)
In this example:
- We import
CSRFProtect
fromflask_wtf.csrf
- We create an instance of
CSRFProtect
and initialize it with our Flask application - We set a secret key, which is used for signing the CSRF tokens
Adding CSRF Token to Templates
For CSRF protection to work, you need to include the CSRF token in your forms. Here's how to do it:
<!-- templates/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>CSRF Protection Example</title>
</head>
<body>
<h1>CSRF Protected Form</h1>
<form method="POST" action="/submit">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div>
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
</div>
<button type="submit">Submit</button>
</form>
</body>
</html>
Notice the hidden input field with the name "csrf_token". This field contains the CSRF token generated by Flask-WTF.
Using WTForms with CSRF Protection
If you're using WTForms (which is recommended), the CSRF token is automatically included when you render the form:
from flask import Flask, render_template, redirect, url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key' # Replace with a strong secret key
class NameForm(FlaskForm):
name = StringField('Name', validators=[DataRequired()])
submit = SubmitField('Submit')
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
return f"Form submitted successfully! Hello, {form.name.data}!"
return render_template('wtform.html', form=form)
if __name__ == '__main__':
app.run(debug=True)
And in your template:
<!-- templates/wtform.html -->
<!DOCTYPE html>
<html>
<head>
<title>WTForms CSRF Example</title>
</head>
<body>
<h1>CSRF Protected Form with WTForms</h1>
<form method="POST">
{{ form.csrf_token }}
<div>
{{ form.name.label }} {{ form.name(size=20) }}
</div>
{{ form.submit() }}
</form>
</body>
</html>
When using WTForms, you just need to include {{ form.csrf_token }}
in your form template.
CSRF Protection for AJAX Requests
If you're making AJAX requests, you'll need to include the CSRF token in your headers. Here's how to do it with JavaScript:
<!-- templates/ajax.html -->
<!DOCTYPE html>
<html>
<head>
<title>AJAX CSRF Example</title>
<script>
function submitForm() {
const name = document.getElementById('name').value;
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ name: name })
})
.then(response => response.text())
.then(data => {
document.getElementById('result').textContent = data;
});
return false;
}
</script>
</head>
<body>
<h1>CSRF Protected AJAX Form</h1>
<meta name="csrf-token" content="{{ csrf_token() }}">
<form onsubmit="return submitForm()">
<div>
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
</div>
<button type="submit">Submit via AJAX</button>
</form>
<div id="result"></div>
</body>
</html>
And the Flask route to handle the AJAX request:
@app.route('/api/submit', methods=['POST'])
def api_submit():
data = request.json
name = data.get('name')
return f"Form submitted successfully via AJAX! Hello, {name}!"
Exempt Routes from CSRF Protection
Sometimes, you may want to exempt certain routes from CSRF protection, such as APIs used by external services. You can do this using the csrf.exempt
decorator:
@app.route('/api/external', methods=['POST'])
@csrf.exempt
def external_api():
# This route is exempt from CSRF protection
return "API endpoint for external services"
However, be careful when exempting routes from CSRF protection. Only do this if you have other security measures in place, such as API keys or tokens.
Best Practices for CSRF Protection
-
Always use HTTPS: CSRF tokens sent over HTTP can be intercepted and used by attackers.
-
Use a strong secret key: Your application's secret key is used to generate CSRF tokens, so make it strong and keep it secret.
-
Don't exempt sensitive routes: Never exempt routes that perform sensitive actions (like changing passwords or making payments) from CSRF protection.
-
Set proper cookie attributes: Use SameSite=Lax or SameSite=Strict for your cookies to provide an additional layer of protection.
-
Don't store tokens in localStorage or sessionStorage: These are accessible by JavaScript and therefore vulnerable to XSS attacks.
Handling CSRF Token Errors
When a CSRF token validation fails, Flask-WTF will raise a CSRFError
exception. You can customize how this error is handled:
from flask_wtf.csrf import CSRFError
@app.errorhandler(CSRFError)
def handle_csrf_error(e):
return render_template('csrf_error.html', reason=e.description), 400
And create a template for the error:
<!-- templates/csrf_error.html -->
<!DOCTYPE html>
<html>
<head>
<title>CSRF Error</title>
</head>
<body>
<h1>CSRF Validation Failed</h1>
<p>{{ reason }}</p>
<p>Please go back and try again.</p>
<a href="/">Back to Home</a>
</body>
</html>
Testing CSRF Protection
To test that your CSRF protection is working correctly, you can try submitting a form without a CSRF token or with an invalid token:
import unittest
from app import app as flask_app
class CSRFTestCase(unittest.TestCase):
def setUp(self):
self.app = flask_app.test_client()
self.app.testing = True
def test_csrf_protection(self):
# This should fail because we're not including a CSRF token
response = self.app.post('/submit', data={
'name': 'Test User'
})
self.assertEqual(response.status_code, 400)
def test_valid_submission(self):
# First, get a CSRF token
response = self.app.get('/')
html = response.data.decode()
# Extract the CSRF token from the HTML
import re
csrf_token = re.search('name="csrf_token" value="(.+?)"', html).group(1)
# Now submit the form with the token
response = self.app.post('/submit', data={
'name': 'Test User',
'csrf_token': csrf_token
})
self.assertEqual(response.status_code, 200)
self.assertIn('Hello, Test User', response.data.decode())
if __name__ == '__main__':
unittest.main()
A Complete Example
Let's put everything together in a complete example:
from flask import Flask, render_template, request, jsonify, redirect, url_for
from flask_wtf import FlaskForm
from flask_wtf.csrf import CSRFProtect, CSRFError
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key' # Replace with a strong secret key
csrf = CSRFProtect(app)
class NameForm(FlaskForm):
name = StringField('Name', validators=[DataRequired()])
submit = SubmitField('Submit')
@app.route('/')
def index():
return render_template('index.html')
@app.route('/wtform', methods=['GET', 'POST'])
def wtform():
form = NameForm()
if form.validate_on_submit():
return f"Form submitted successfully! Hello, {form.name.data}!"
return render_template('wtform.html', form=form)
@app.route('/ajax')
def ajax():
return render_template('ajax.html')
@app.route('/submit', methods=['POST'])
def submit():
name = request.form.get('name')
return f"Form submitted successfully! Hello, {name}!"
@app.route('/api/submit', methods=['POST'])
def api_submit():
data = request.json
name = data.get('name')
return f"Form submitted successfully via AJAX! Hello, {name}!"
@app.route('/api/external', methods=['POST'])
@csrf.exempt
def external_api():
return "API endpoint for external services"
@app.errorhandler(CSRFError)
def handle_csrf_error(e):
return render_template('csrf_error.html', reason=e.description), 400
if __name__ == '__main__':
app.run(debug=True)
Summary
CSRF protection is an essential security feature for any web application that handles user authentication and form submissions. Flask makes it easy to implement this protection through the Flask-WTF extension.
In this tutorial, you've learned:
- How CSRF attacks work and why they're dangerous
- How to set up CSRF protection in Flask applications
- How to include CSRF tokens in regular forms and AJAX requests
- Best practices for implementing CSRF protection
- How to test that your CSRF protection is working correctly
By following these guidelines, you can protect your Flask application and your users from CSRF attacks.
Additional Resources
Exercises
- Create a Flask application with a registration form that includes CSRF protection.
- Implement a simple blog application where users can create and edit posts, with CSRF protection for all forms.
- Create a REST API with Flask and implement token-based authentication as an alternative to CSRF tokens.
- Modify the AJAX example to handle CSRF errors gracefully and show an error message to the user.
- Research and implement additional security headers (like Content-Security-Policy) to further protect your application from attacks.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)