Flask Security Checklist
Introduction
Security is a critical aspect of web application development that can't be overlooked. Flask, being a lightweight web framework, doesn't come with many security features enabled by default. This makes it essential for developers to implement proper security measures themselves.
This guide provides a comprehensive security checklist for Flask applications, helping beginners understand and implement essential security practices. By following these recommendations, you'll be able to protect your application against common vulnerabilities and threats.
Why Security Matters in Flask Applications
Before diving into specific security measures, it's important to understand why security is crucial:
- Data Protection - Your application may handle sensitive user data
- Service Availability - Security breaches can disrupt your service
- User Trust - Security incidents can damage user confidence
- Compliance - Many industries have legal requirements for data security
Essential Flask Security Measures
1. Configure Proper Secret Keys
Flask uses a secret key for signing cookies and other security-related functions. Never use a hardcoded or easy-to-guess secret key in production.
# BAD - Don't do this in production
app.secret_key = 'easy_to_guess_key'
# GOOD - Use a strong, random secret key
import os
app.secret_key = os.urandom(24)
# BEST - Load from environment variable
app.secret_key = os.environ.get('SECRET_KEY')
For production applications, generate a strong key once and store it as an environment variable:
# In your terminal, generate a key
$ python -c 'import os; print(os.urandom(24).hex())'
3d6f45a5fc12445dbac2f59c3b6c7cb1
# Set as environment variable
$ export SECRET_KEY="3d6f45a5fc12445dbac2f59c3b6c7cb1"
2. Protect Against CSRF Attacks
Cross-Site Request Forgery (CSRF) attacks trick users into submitting unwanted actions. Use Flask-WTF to protect against CSRF:
from flask import Flask, render_template
from flask_wtf.csrf import CSRFProtect
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
csrf = CSRFProtect(app)
class SimpleForm(FlaskForm):
content = StringField('Content')
submit = SubmitField('Submit')
@app.route('/form', methods=['GET', 'POST'])
def form():
form = SimpleForm()
if form.validate_on_submit():
# Process the form data
return "Form submitted successfully!"
return render_template('form.html', form=form)
In your template (form.html
):
<form method="post">
{{ form.csrf_token }}
{{ form.content.label }} {{ form.content(size=20) }}
{{ form.submit() }}
</form>
3. Secure Cookie Settings
Cookies should be configured securely to prevent various attacks:
app = Flask(__name__)
app.config.update(
SESSION_COOKIE_SECURE=True, # Only send cookies over HTTPS
SESSION_COOKIE_HTTPONLY=True, # Prevent JavaScript access to cookies
SESSION_COOKIE_SAMESITE='Lax', # Restrict cross-site requests
PERMANENT_SESSION_LIFETIME=1800 # Session timeout (30 minutes)
)
4. Input Validation and Sanitization
Never trust user input. Always validate and sanitize data before processing:
from werkzeug.utils import escape
@app.route('/user/<username>')
def user_profile(username):
# Sanitize input to prevent XSS
safe_username = escape(username)
# Use the sanitized input
return f"Profile for {safe_username}"
For form validation, use WTForms:
from wtforms.validators import DataRequired, Email, Length
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(),
Length(min=3, max=20)
])
email = StringField('Email', validators=[
DataRequired(),
Email()
])
# Other fields...
5. Database Security
When using an ORM like SQLAlchemy, use parameterized queries to prevent SQL injection:
# BAD - vulnerable to SQL injection
@app.route('/user_search')
def user_search():
username = request.args.get('username')
query = f"SELECT * FROM users WHERE username='{username}'" # DANGER!
# ...
# GOOD - Use ORM with parameterized queries
@app.route('/user_search')
def user_search():
username = request.args.get('username')
user = User.query.filter_by(username=username).first()
# ...
6. Password Storage
Never store plain-text passwords. Use a library like werkzeug.security
or passlib
to hash passwords:
from werkzeug.security import generate_password_hash, check_password_hash
# When creating a new user
password = request.form.get('password')
hashed_password = generate_password_hash(password, method='pbkdf2:sha256')
# Store hashed_password in database
# When verifying a password
stored_hash = user.password_hash # Retrieved from database
password_valid = check_password_hash(stored_hash, submitted_password)
7. Content Security Policy (CSP)
Implement CSP headers to prevent XSS attacks:
from flask import Flask
app = Flask(__name__)
@app.after_request
def add_security_headers(response):
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self'"
return response
8. Rate Limiting
Protect against brute force attacks by implementing rate limiting:
from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute") # More strict limit for login attempts
def login():
# Login logic here
return "Login process"
9. HTTPS Enforcement
Ensure your application forces HTTPS connections:
from flask_talisman import Talisman
app = Flask(__name__)
talisman = Talisman(
app,
force_https=True,
strict_transport_security=True,
strict_transport_security_max_age=31536000 # 1 year in seconds
)
10. Security Headers
Implement security headers for your Flask application:
@app.after_request
def security_headers(response):
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
return response
Practical Example: Secure Flask Application
Let's build a simple but secure Flask application incorporating multiple security practices:
import os
from flask import Flask, render_template, request, redirect, url_for, session, flash
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length
from werkzeug.security import generate_password_hash, check_password_hash
from flask_talisman import Talisman
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
# Security configurations
app.config.update(
SECRET_KEY=os.environ.get('SECRET_KEY', os.urandom(24)),
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='Lax',
PERMANENT_SESSION_LIFETIME=1800
)
# Setup security extensions
talisman = Talisman(
app,
force_https=True,
content_security_policy={
'default-src': "'self'",
'style-src': "'self' 'unsafe-inline'"
}
)
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
# Mock user database
users = {
"[email protected]": {
"password": generate_password_hash("secure_password"),
"name": "Admin User"
}
}
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Login')
@app.route('/')
def home():
return render_template('home.html', user=session.get('user'))
@app.route('/login', methods=['GET', 'POST'])
@limiter.limit("5 per minute") # Rate limiting for login attempts
def login():
form = LoginForm()
if form.validate_on_submit():
email = form.email.data
password = form.password.data
if email in users and check_password_hash(users[email]["password"], password):
session['user'] = users[email]["name"]
flash('Login successful!', 'success')
return redirect(url_for('home'))
else:
flash('Invalid email or password', 'error')
return render_template('login.html', form=form)
@app.route('/logout')
def logout():
session.pop('user', None)
flash('You have been logged out', 'info')
return redirect(url_for('home'))
@app.after_request
def add_security_headers(response):
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
return response
if __name__ == '__main__':
app.run(debug=False) # Set to False in production
To complete this example, you would need these templates:
home.html
<!DOCTYPE html>
<html>
<head>
<title>Secure Flask App</title>
</head>
<body>
<h1>Welcome to Secure Flask App</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% if user %}
<p>Hello, {{ user }}!</p>
<a href="{{ url_for('logout') }}">Logout</a>
{% else %}
<p>Please <a href="{{ url_for('login') }}">login</a> to continue.</p>
{% endif %}
</body>
</html>
login.html
<!DOCTYPE html>
<html>
<head>
<title>Login - Secure Flask App</title>
</head>
<body>
<h1>Login</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="post">
{{ form.csrf_token }}
<div>
{{ form.email.label }}<br>
{{ form.email() }}
</div>
<div>
{{ form.password.label }}<br>
{{ form.password() }}
</div>
<div>
{{ form.submit() }}
</div>
</form>
<a href="{{ url_for('home') }}">Back to Home</a>
</body>
</html>
Regular Security Maintenance
Security is not a one-time implementation but an ongoing process:
-
Keep Dependencies Updated
bashpip install --upgrade flask
pip freeze > requirements.txt -
Run Security Scans Tools like Bandit can identify security issues in your Python code:
bashpip install bandit
bandit -r your_flask_app/ -
Regularly Audit Your Code Schedule periodic reviews of your codebase focusing on security.
Security Testing Checklist
Here's a quick checklist for testing your Flask application's security:
- Verify HTTPS is enforced
- Test CSRF protection by attempting to forge requests
- Attempt SQL injection on form inputs
- Try XSS attacks to see if they're properly blocked
- Test password strength requirements
- Check if rate limiting effectively blocks excessive requests
- Verify that sessions expire correctly
- Ensure proper error handling that doesn't expose sensitive information
Summary
This guide has covered essential security practices for Flask applications:
- Proper secret key configuration
- CSRF protection
- Secure cookie settings
- Input validation and sanitization
- Database security measures
- Secure password storage
- Content Security Policy implementation
- Rate limiting to prevent abuse
- HTTPS enforcement
- Security headers
By implementing these measures, you'll significantly improve the security posture of your Flask applications. Remember that security is an ongoing process that requires regular updates, monitoring, and testing.
Additional Resources
- Flask Security Documentation
- OWASP Top Ten - Common web application vulnerabilities
- Flask-Security Extension
- Flask-Talisman Documentation
- OWASP Cheat Sheet Series
Practice Exercises
- Implement a complete user authentication system with password reset functionality using the security practices outlined above.
- Create a Flask API with token-based authentication and proper rate limiting.
- Set up a CI/CD pipeline that includes security scanning using tools like Bandit or OWASP ZAP.
- Perform a security audit on an existing Flask application and document the vulnerabilities found.
By following this security checklist, you'll be well on your way to developing more secure Flask applications that protect your users' data and maintain their trust.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)