Flask applications are surprisingly vulnerable to XSS, CSRF, and injection attacks if you don’t explicitly address them.
Let’s see it in action. Imagine a simple Flask app with a search feature that just echoes user input:
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def index():
query = request.args.get('q', '')
return render_template_string(f'<h1>Search Results for: {query}</h1>')
if __name__ == '__main__':
app.run(debug=True)
If a user searches for <script>alert('XSS!')</script>, the browser will execute that JavaScript. Similarly, if this app were to use query in a database query without sanitization, it would be vulnerable to SQL injection. CSRF is a bit more subtle, but it allows an attacker to trick a logged-in user into performing an action they didn’t intend.
To build a robust mental model, we need to tackle these threats individually.
Cross-Site Scripting (XSS)
XSS happens when an attacker injects malicious scripts into web pages viewed by other users. In our example, render_template_string directly embeds query. Flask’s default template engine (Jinja2) does autoescape HTML, but render_template_string with f-strings bypasses this.
-
Diagnosis: Test by submitting HTML/JavaScript payloads in input fields. For example,
?q=<script>alert(1)</script>. -
Fix: Always use proper templating and avoid direct string formatting for user-provided content. Even better, explicitly escape.
from flask import Flask, request, render_template import html app = Flask(__name__) @app.route('/') def index(): query = request.args.get('q', '') # Using render_template with a separate HTML file is preferred # For this example, we'll simulate proper escaping within render_template_string # In a real app, use a .html file and Jinja2's autoescaping escaped_query = html.escape(query) return render_template_string(f'<h1>Search Results for: {escaped_query}</h1>') if __name__ == '__main__': app.run(debug=True)This
html.escape()call converts characters like<to<, preventing them from being interpreted as HTML tags.
Cross-Site Request Forgery (CSRF)
CSRF allows an attacker to induce a logged-in user’s browser to send a forged HTTP request to a vulnerable web application, making it appear as though the user initiated the request. This is particularly dangerous for actions like changing passwords or making purchases.
-
Diagnosis: Identify any form submissions or API endpoints that perform state-changing actions (POST, PUT, DELETE) but don’t implement CSRF protection.
-
Fix: Use Flask-WTF and WTForms to generate CSRF tokens.
First, install Flask-WTF:
pip install Flask-WTFThen, configure your app:
from flask import Flask, request, render_template, flash from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from wtforms.validators import DataRequired import html app = Flask(__name__) app.config['SECRET_KEY'] = 'a_very_secret_key_change_this_in_prod' # Crucial for CSRF class SearchForm(FlaskForm): q = StringField('Search', validators=[DataRequired()]) submit = SubmitField('Search') @app.route('/', methods=['GET', 'POST']) def index(): form = SearchForm() query = '' if form.validate_on_submit(): query = form.q.data # In a real app, this would be a search action flash(f"Searching for: {query}") escaped_query = html.escape(query) # Still important for rendering return render_template_string(''' <form method="POST"> {{ form.csrf_token }} {# This is the magic #} {{ form.q.label }} {{ form.q() }} {{ form.submit() }} </form> <h1>Search Results for: {{ escaped_query }}</h1> ''', form=form, escaped_query=escaped_query) if __name__ == '__main__': app.run(debug=True)The
{{ form.csrf_token }}line injects a hidden field containing a unique token into your form. On submission, Flask-WTF verifies this token against the one stored in the user’s session, ensuring the request originated from your site. TheSECRET_KEYis vital for signing session cookies and generating these tokens.
Injection Attacks
Injection attacks occur when an attacker can send untrusted data to an interpreter, which then executes that data as code. The most common is SQL injection, but command injection and others are also possible.
-
Diagnosis: Look for any instance where user input is directly used in database queries, shell commands, or other interpreters without sanitization or parameterization.
-
Fix: Always use parameterized queries or ORMs for database interactions. For shell commands, avoid passing user input directly.
If you were using direct string formatting for SQL:
cursor.execute(f"SELECT * FROM users WHERE username = '{username}'")The correct, parameterized way:
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))This tells the database driver to treatusernamepurely as data, not as SQL code, preventing injection. Similarly, for shell commands, use libraries likesubprocesswith its argument list features rather thanos.system.
The one thing that often trips people up with Flask’s security features is the interplay between different components. For instance, relying on Jinja2’s autoescaping might seem sufficient for XSS, but if you bypass it with f-strings or by manually disabling escaping in specific contexts, you reintroduce vulnerabilities. Similarly, CSRF protection is tied to your application’s secret key and session management; if those are compromised or misconfigured, your CSRF tokens become useless.
The next logical step in securing your Flask app is understanding session management and cookie security.