Skip to main content

Flask: Preventing XSS and SQL Injection

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