Flask: Preventing XSS and SQL Injection
Cross-Site Scripting (XSS) and SQL Injection are critical web vulnerabilities that can compromise user data and application integrity. Flask, a lightweight Python web framework, requires explicit measures to prevent these attacks due to its minimalistic design. This tutorial explores preventing XSS and SQL Injection in Flask applications, covering secure input handling, template rendering, database queries, and best practices for robust security.
01. Why Prevent XSS and SQL Injection?
XSS allows attackers to inject malicious scripts into web pages, potentially stealing user data or hijacking sessions. SQL Injection enables unauthorized database access, leading to data leaks or manipulation. Flask’s flexibility demands proactive security measures, leveraging tools like Jinja2’s auto-escaping and SQLAlchemy’s parameterized queries to mitigate these risks, aligning with OWASP guidelines.
Example: Basic XSS Prevention with Jinja2
from flask import Flask, render_template_string, request
app = Flask(__name__)
@app.route('/greet', methods=['GET', 'POST'])
def greet():
name = request.form.get('name', 'Guest') if request.method == 'POST' else 'Guest'
return render_template_string('<h1>Hello, {{ name }}!</h1>', name=name)
if __name__ == '__main__':
app.run(debug=True)
Output:
* Running on http://127.0.0.1:5000
(POST to /greet with name="<script>alert('XSS')</script>": Renders as text, not executable script)
Explanation:
- Jinja2 auto-escapes HTML in templates, preventing XSS by rendering scripts as text.
render_template_string
- Safely handles user input in templates.
02. Key Prevention Techniques
Preventing XSS and SQL Injection in Flask involves secure input handling, safe database queries, and proper output encoding. These techniques protect against malicious inputs and ensure data integrity. The table below summarizes key techniques and their applications:
Technique | Description | Use Case |
---|---|---|
Template Auto-Escaping | Escape HTML in Jinja2 templates | Prevent XSS in user-facing content |
Input Sanitization | Validate and clean user input | Block malicious scripts or queries |
Parameterized Queries | Use SQLAlchemy or prepared statements | Prevent SQL Injection |
Content Security Policy (CSP) | Restrict script sources | Mitigate XSS impact |
Escaping in APIs | Sanitize JSON or API outputs | Prevent XSS in API responses |
2.1 Template Auto-Escaping for XSS Prevention
Example: Safe Template Rendering
from flask import Flask, render_template_string, request
app = Flask(__name__)
@app.route('/comment', methods=['GET', 'POST'])
def comment():
comment = request.form.get('comment', '') if request.method == 'POST' else ''
return render_template_string('''
<h1>Comment</h1>
<p>{{ comment }}</p>
<form method="post">
<input type="text" name="comment">
<input type="submit" value="Submit">
</form>
''', comment=comment)
if __name__ == '__main__':
app.run(debug=True)
Output:
* Running on http://127.0.0.1:5000
(POST to /comment with comment="<script>alert('XSS')</script>": Renders as escaped text: <script>alert('XSS')</script>)
Explanation:
- Jinja2 escapes special characters (
<
,>
) by default, preventing script execution. - Avoid
{{ comment | safe }}
unless the input is trusted and sanitized.
2.2 Input Sanitization with Flask-WTF
Example: Validating Form Input
from flask import Flask, render_template_string, request
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Length
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
class CommentForm(FlaskForm):
comment = StringField('Comment', validators=[DataRequired(), Length(max=200)])
submit = SubmitField('Submit')
@app.route('/secure_comment', methods=['GET', 'POST'])
def secure_comment():
form = CommentForm()
if form.validate_on_submit():
return f"Comment: {form.comment.data}"
return render_template_string('''
<form method="post">
{{ form.hidden_tag() }}
{{ form.comment.label }} {{ form.comment }}<br>
{{ form.submit }}
</form>
''', form=form)
if __name__ == '__main__':
app.run(debug=True)
Output:
* Running on http://127.0.0.1:5000
(POST to /secure_comment with valid input: Returns "Comment: Hello")
(POST with invalid input: Returns form with validation errors)
Explanation:
Flask-WTF
- Validates input length and presence, reducing XSS risks.- CSRF token (
form.hidden_tag()
) adds additional security.
2.3 Parameterized Queries for SQL Injection Prevention
Example: Safe Database Queries with SQLAlchemy
from flask import Flask, request
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50))
@app.route('/user', methods=['GET'])
def get_user():
username = request.args.get('username')
user = User.query.filter_by(username=username).first()
return f"User: {user.username}" if user else "User not found"
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
Output:
* Running on http://127.0.0.1:5000
(GET to /user?username=Alice: Returns "User: Alice")
(GET with malicious input like "'; DROP TABLE users; --": Safely handled by SQLAlchemy)
Explanation:
filter_by
- Uses parameterized queries, preventing SQL Injection.- SQLAlchemy escapes inputs, ensuring safe database interactions.
2.4 Content Security Policy (CSP) for XSS Mitigation
Example: Adding CSP Headers
from flask import Flask, render_template_string, request
app = Flask(__name__)
@app.after_request
def add_csp_header(response):
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self'"
return response
@app.route('/csp_example', methods=['GET', 'POST'])
def csp_example():
content = request.form.get('content', '') if request.method == 'POST' else ''
return render_template_string('''
<h1>Content</h1>
<p>{{ content }}</p>
<form method="post">
<input type="text" name="content">
<input type="submit" value="Submit">
</form>
''', content=content)
if __name__ == '__main__':
app.run(debug=True)
Output:
* Running on http://127.0.0.1:5000
(POST to /csp_example with malicious script: Browser blocks external or inline scripts due to CSP)
Explanation:
Content-Security-Policy
- Restricts script sources to the same origin, mitigating XSS.- Reduces impact of unescaped scripts in case of vulnerabilities.
2.5 Escaping in API Responses
Example: Safe JSON Responses
from flask import Flask, jsonify, request
from markupsafe import escape
app = Flask(__name__)
@app.route('/api/comment', methods=['POST'])
def api_comment():
comment = request.json.get('comment', '')
sanitized_comment = str(escape(comment)) # Escape HTML characters
return jsonify({'comment': sanitized_comment})
if __name__ == '__main__':
app.run(debug=True)
Output:
* Running on http://127.0.0.1:5000
(POST to /api/comment with {"comment": "<script>alert('XSS')</script>"}: Returns {"comment": "<script>alert('XSS')</script>"})
Explanation:
markupsafe.escape
- Sanitizes input for safe API responses.- Prevents XSS when API data is rendered in client-side applications.
2.6 Insecure Query Example
Example: Vulnerable SQL Injection
from flask import Flask, request
from sqlite3 import connect
app = Flask(__name__)
@app.route('/user', methods=['GET'])
def get_user():
username = request.args.get('username')
conn = connect('users.db')
cursor = conn.cursor()
# Vulnerable: Direct string concatenation
cursor.execute(f"SELECT username FROM users WHERE username = '{username}'")
user = cursor.fetchone()
conn.close()
return f"User: {user[0]}" if user else "User not found"
if __name__ == '__main__':
app.run(debug=True)
Output:
* Running on http://127.0.0.1:5000
(GET to /user?username=Alice: Works as expected)
(GET with username="'; DROP TABLE users; --": Executes malicious SQL)
Explanation:
- String concatenation in SQL queries allows injection attacks.
- Solution: Use parameterized queries or an ORM like SQLAlchemy.
03. Effective Usage
3.1 Recommended Practices
- Always use parameterized queries or ORMs for database operations.
Example: Comprehensive XSS and SQL Injection Prevention
from flask import Flask, render_template_string, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Length
from markupsafe import escape
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///secure.db'
app.config['SECRET_KEY'] = 'your-secret-key'
db = SQLAlchemy(app)
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.String(200))
class CommentForm(FlaskForm):
content = StringField('Comment', validators=[DataRequired(), Length(max=200)])
submit = SubmitField('Submit')
@app.after_request
def add_csp_header(response):
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self'"
return response
@app.route('/comments', methods=['GET', 'POST'])
def comments():
form = CommentForm()
if form.validate_on_submit():
comment = Comment(content=form.content.data)
db.session.add(comment)
db.session.commit()
return render_template_string('''
<h1>Comment Added</h1>
<p>{{ comment }}</p>
<a href="{{ url_for('comments') }}">Back</a>
''', comment=form.content.data)
comments = Comment.query.all()
return render_template_string('''
<h1>Comments</h1>
{% for comment in comments %}
<p>{{ comment.content }}</p>
{% endfor %}
<form method="post">
{{ form.hidden_tag() }}
{{ form.content.label }} {{ form.content }}<br>
{{ form.submit }}
</form>
''', form=form, comments=comments)
@app.route('/api/comments', methods=['POST'])
def api_comments():
content = request.json.get('content', '')
sanitized_content = str(escape(content))
comment = Comment(content=sanitized_content)
db.session.add(comment)
db.session.commit()
return jsonify({'message': 'Comment added', 'content': sanitized_content})
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
Output:
* Running on http://127.0.0.1:5000
(POST to /comments with valid input: Adds comment, renders safely)
(POST to /api/comments with malicious script: Returns sanitized JSON)
(Malicious inputs: Blocked by validation, escaping, and CSP)
- Combines Jinja2 auto-escaping, SQLAlchemy parameterized queries, Flask-WTF validation, CSP, and API sanitization.
- Ensures robust protection against XSS and SQL Injection.
3.2 Practices to Avoid
- Avoid disabling Jinja2 auto-escaping or using raw SQL queries.
Example: Vulnerable XSS with Unsafe Template
from flask import Flask, render_template_string, request
app = Flask(__name__)
@app.route('/unsafe', methods=['GET', 'POST'])
def unsafe():
content = request.form.get('content', '') if request.method == 'POST' else ''
return render_template_string('''
<h1>Content</h1>
<p>{{ content | safe }}</p>
<form method="post">
<input type="text" name="content">
<input type="submit" value="Submit">
</form>
''', content=content)
if __name__ == '__main__':
app.run(debug=True)
Output:
* Running on http://127.0.0.1:5000
(POST to /unsafe with "<script>alert('XSS')</script>": Executes script in browser)
| safe
- Bypasses Jinja2 escaping, enabling XSS.- Solution: Avoid
safe
filter unless input is fully sanitized.
04. Common Use Cases
4.1 Secure User Comments
Safely handle user-submitted comments to prevent XSS and SQL Injection.
Example: Secure Comment System
from flask import Flask, render_template_string
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///comments.db'
app.config['SECRET_KEY'] = 'your-secret-key'
db = SQLAlchemy(app)
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.String(500))
class CommentForm(FlaskForm):
content = TextAreaField('Comment', validators=[DataRequired(), Length(max=500)])
submit = SubmitField('Submit')
@app.route('/comments', methods=['GET', 'POST'])
def comments():
form = CommentForm()
if form.validate_on_submit():
comment = Comment(content=form.content.data)
db.session.add(comment)
db.session.commit()
return render_template_string('''
<h1>Comment Added</h1>
<p>{{ comment }}</p>
<a href="{{ url_for('comments') }}">Back</a>
''', comment=form.content.data)
comments = Comment.query.all()
return render_template_string('''
<h1>Comments</h1>
{% for comment in comments %}
<p>{{ comment.content }}</p>
{% endfor %}
<form method="post">
{{ form.hidden_tag() }}
{{ form.content.label }} {{ form.content }}<br>
{{ form.submit }}
</form>
''', form=form, comments=comments)
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
Output:
* Running on http://127.0.0.1:5000
(POST to /comments with valid input: Adds comment, renders safely)
(Malicious input like "<script>": Rendered as text, stored safely)
Explanation:
- Uses
Flask-WTF
for input validation and CSRF protection. - SQLAlchemy ensures safe database storage.
4.2 Secure API Endpoints
Protect API endpoints from XSS and SQL Injection in JSON responses.
Example: Secure API with Sanitization
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy
from markupsafe import escape
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.db'
db = SQLAlchemy(app)
class Record(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.String(200))
@app.route('/api/record', methods=['POST'])
def add_record():
content = request.json.get('content', '')
sanitized_content = str(escape(content))
record = Record(content=sanitized_content)
db.session.add(record)
db.session.commit()
return jsonify({'id': record.id, 'content': sanitized_content})
@app.route('/api/record/<int:id>', methods=['GET'])
def get_record(id):
record = Record.query.get_or_404(id)
return jsonify({'id': record.id, 'content': record.content})
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
Output:
* Running on http://127.0.0.1:5000
(POST to /api/record with {"content": "<script>alert('XSS')</script>"}: Returns {"id": 1, "content": "<script>alert('XSS')</script>"})
(GET to /api/record/1: Returns sanitized content)
Explanation:
- Sanitizes input with
escape
for safe API output. - SQLAlchemy prevents SQL Injection during storage.
Conclusion
Preventing XSS and SQL Injection in Flask requires careful input handling, secure database queries, and output sanitization. Key takeaways:
- Rely on Jinja2 auto-escaping and avoid
| safe
for untrusted input. - Use
Flask-WTF
for input validation and CSRF protection. - Employ parameterized queries with SQLAlchemy to block SQL Injection.
- Add CSP headers and sanitize API responses to enhance XSS protection.
With these practices, you can build Flask applications that are resilient to XSS and SQL Injection, ensuring user safety and data integrity!
Comments
Post a Comment