XSS in Flask Applications
Flask uses Jinja2 templates which auto-escape HTML by default, but the |safe filter and Markup() function let developers bypass this protection. Flask's lightweight nature means security features like CSP must be added manually, and many Flask tutorials skip these critical configurations.
Scan Your Flask AppHow XSS Manifests in Flask
Flask's Jinja2 templates auto-escape {{ variable }} output, but developers bypass this with |safe for rendering rich text, markdown, and HTML emails. The Markup() class in Python views marks strings as safe, skipping template escaping. Flask's make_response() and direct string returns from route handlers do not apply any escaping. Returning HTML strings that include user data (like f"<h1>{request.args.get('q')}</h1>") is a direct XSS vulnerability. Flash messages rendered with |safe are another common vector, as developers want to include HTML formatting in notifications.
Real-World Impact
A Flask admin panel used the |safe filter to render user support tickets with HTML formatting. An attacker submitted a ticket containing JavaScript that executed when an admin viewed it, granting the attacker access to the admin API endpoints and all user data in the system.
Step-by-Step Fix
Install Flask-Talisman for security headers
Flask-Talisman adds CSP, HTTPS enforcement, and other security headers.
from flask import Flask
from flask_talisman import Talisman
app = Flask(__name__)
talisman = Talisman(
app,
content_security_policy={
'default-src': "'self'",
'script-src': "'self'",
'style-src': "'self' 'unsafe-inline'",
},
)Sanitize HTML before using |safe
Use bleach to clean HTML content before marking it as safe.
import bleach
from markupsafe import Markup
ALLOWED_TAGS = ['b', 'i', 'em', 'strong', 'a', 'p', 'br']
ALLOWED_ATTRS = {'a': ['href', 'target', 'rel']}
@app.route('/post/<int:post_id>')
def view_post(post_id):
post = get_post(post_id)
clean_html = bleach.clean(
post.content,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRS,
strip=True
)
return render_template('post.html', content=Markup(clean_html))Avoid raw HTML in route responses
Never interpolate user data into HTML string responses.
# UNSAFE
@app.route('/search')
def search():
q = request.args.get('q', '')
return f'<h1>Results for: {q}</h1>'
# SAFE - use templates
@app.route('/search')
def search():
q = request.args.get('q', '')
return render_template('search.html', query=q)
<!-- search.html: auto-escapes -->
<h1>Results for: {{ query }}</h1>Prevention Best Practices
1. Never use |safe or Markup() with user-controlled data without sanitization. 2. Use bleach or nh3 to sanitize HTML before marking as safe. 3. Install flask-talisman for automatic CSP headers. 4. Return JSON from API routes instead of HTML strings. 5. Escape flash messages or avoid using |safe with them.
How to Test
1. Search for |safe and Markup() in your codebase. 2. Test with <script>alert(1)</script> in any field rendered with |safe. 3. Check route handlers that return HTML strings with user data. 4. Test flash messages for XSS if they use |safe in templates. 5. Use Vibe App Scanner to automatically detect XSS patterns in your Flask application.
Frequently Asked Questions
Does Flask/Jinja2 auto-escape HTML?
Yes, Jinja2 auto-escapes HTML in {{ variable }} expressions by default when used with Flask. However, the |safe filter and Markup() function bypass this protection. Always sanitize content before using these escape hatches.
What is the difference between bleach and nh3 for Flask?
Bleach is a Python library for sanitizing HTML that is now in maintenance mode. nh3 is a newer, Rust-based alternative that is faster and actively maintained. Both work well with Flask; nh3 is recommended for new projects.
How do I add CSP headers to Flask?
Use the Flask-Talisman extension which provides CSP and other security headers. Alternatively, use the @app.after_request decorator to add headers manually to every response.
Related Security Resources
Is Your Flask App Vulnerable to XSS?
VAS automatically scans for xss vulnerabilities in Flask applications and provides step-by-step remediation guidance with code examples.
Scans from $5, results in minutes. Get actionable fixes tailored to your Flask stack.