Flask Security
Security is a critical aspect of web application development. This guide covers essential security practices for Flask applications.
Authentication and Authorization
Password Hashing
Never store passwords in plain text. Use a secure hashing library:
python
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
# Usage
user = User(username='john')
user.set_password('secret123')
db.session.add(user)
db.session.commit()
# Verify password
if user.check_password('secret123'):
print('Password is correct')Session Management
Use Flask-Login for secure session management:
python
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
class User(UserMixin, db.Model):
# ... model definition
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
@app.route('/login', methods=['POST'])
def login():
user = User.query.filter_by(username=request.form['username']).first()
if user and user.check_password(request.form['password']):
login_user(user)
return redirect(url_for('index'))
return 'Invalid credentials'
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('index'))JWT Authentication
For API authentication, use JSON Web Tokens:
python
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
app.config['JWT_SECRET_KEY'] = 'super-secret-key' # Change this!
jwt = JWTManager(app)
@app.route('/api/login', methods=['POST'])
def api_login():
username = request.json.get('username')
password = request.json.get('password')
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
access_token = create_access_token(identity=username)
return jsonify(access_token=access_token)
return jsonify({"msg": "Bad credentials"}), 401
@app.route('/api/protected', methods=['GET'])
@jwt_required()
def protected():
current_user = get_jwt_identity()
return jsonify(logged_in_as=current_user)CSRF Protection
Enable CSRF Protection
python
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect()
csrf.init_app(app)
# In templates
<form method="post">
{{ form.csrf_token }}
<!-- form fields -->
</form>Exempt API Routes
python
from flask_wtf.csrf import csrf_exempt
@app.route('/api/webhook', methods=['POST'])
@csrf_exempt
def webhook():
# Process webhook
return jsonify(success=True)SQL Injection Prevention
Use Parameterized Queries
python
# Bad - vulnerable to SQL injection
username = request.form['username']
query = f"SELECT * FROM users WHERE username = '{username}'"
db.session.execute(query)
# Good - using SQLAlchemy ORM
username = request.form['username']
user = User.query.filter_by(username=username).first()
# Good - using parameterized raw SQL
username = request.form['username']
query = "SELECT * FROM users WHERE username = :username"
result = db.session.execute(query, {'username': username})XSS (Cross-Site Scripting) Prevention
Auto-Escape Templates
Jinja2 auto-escapes by default, but be careful with |safe filter:
html
<!-- Safe - auto-escaped -->
<p>{{ user_input }}</p>
<!-- Dangerous - not escaped -->
<p>{{ user_input|safe }}</p>
<!-- Safe way to allow some HTML -->
<p>{{ user_input|striptags }}</p>Sanitize User Input
python
from markupsafe import escape
@app.route('/comment', methods=['POST'])
def add_comment():
comment = escape(request.form['comment'])
# Save comment
return 'Comment added'Secure Headers
Implement Security Headers
python
from flask_talisman import Talisman
# Force HTTPS and set security headers
Talisman(app,
force_https=True,
strict_transport_security=True,
content_security_policy={
'default-src': "'self'",
'script-src': "'self' 'unsafe-inline'",
'style-src': "'self' 'unsafe-inline'"
}
)
# Or manually set headers
@app.after_request
def set_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['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
return responseCORS (Cross-Origin Resource Sharing)
Configure CORS Properly
python
from flask_cors import CORS
# Allow all origins (development only)
CORS(app)
# Production - specific origins
CORS(app, resources={
r"/api/*": {
"origins": ["https://yourdomain.com"],
"methods": ["GET", "POST"],
"allow_headers": ["Content-Type", "Authorization"]
}
})File Upload Security
Validate File Uploads
python
import os
from werkzeug.utils import secure_filename
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}
MAX_FILE_SIZE = 16 * 1024 * 1024 # 16MB
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return 'No file part', 400
file = request.files['file']
if file.filename == '':
return 'No selected file', 400
if not allowed_file(file.filename):
return 'File type not allowed', 400
# Check file size
file.seek(0, os.SEEK_END)
file_length = file.tell()
if file_length > MAX_FILE_SIZE:
return 'File too large', 400
file.seek(0)
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return 'File uploaded successfully'Rate Limiting
Implement Rate Limiting
python
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@app.route('/api/login', methods=['POST'])
@limiter.limit("5 per minute")
def login():
# Login logic
pass
@app.route('/api/data')
@limiter.limit("100 per hour")
def get_data():
# Return data
passEnvironment Variables and Secrets
Secure Configuration
python
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or os.urandom(32)
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY')
# Never commit these to version control
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD').env File (Never commit to Git)
bash
# .env
SECRET_KEY=your-secret-key-here
DATABASE_URL=postgresql://user:pass@localhost/dbname
JWT_SECRET_KEY=jwt-secret-key
MAIL_USERNAME=your-email@example.com
MAIL_PASSWORD=your-email-password.gitignore
gitignore
.env
.env.local
.env.production
*.pyc
__pycache__/
instance/
.pytest_cache/Input Validation
Validate All User Input
python
from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField, validators
class UserForm(FlaskForm):
username = StringField('Username', [
validators.Length(min=3, max=20),
validators.Regexp('^[A-Za-z0-9_]+$', message='Username must contain only letters, numbers, and underscores')
])
age = IntegerField('Age', [
validators.NumberRange(min=0, max=150)
])
email = StringField('Email', [
validators.Email(message='Invalid email address')
])
@app.route('/register', methods=['POST'])
def register():
form = UserForm()
if form.validate_on_submit():
# Process registration
pass
else:
return jsonify(errors=form.errors), 400API Security
Implement API Key Authentication
python
from functools import wraps
def require_api_key(f):
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if not api_key or api_key != app.config['API_KEY']:
return jsonify(error='Invalid API key'), 401
return f(*args, **kwargs)
return decorated_function
@app.route('/api/data')
@require_api_key
def api_data():
return jsonify(data='sensitive information')Logging and Monitoring
Log Security Events
python
import logging
security_logger = logging.getLogger('security')
security_logger.setLevel(logging.WARNING)
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
user = User.query.filter_by(username=username).first()
if user and user.check_password(request.form['password']):
login_user(user)
app.logger.info(f'Successful login: {username}')
return redirect(url_for('index'))
else:
security_logger.warning(f'Failed login attempt: {username} from {request.remote_addr}')
return 'Invalid credentials', 401Security Checklist
- [ ] Use HTTPS in production
- [ ] Hash passwords with strong algorithms
- [ ] Implement CSRF protection
- [ ] Validate and sanitize all user input
- [ ] Use parameterized queries to prevent SQL injection
- [ ] Set secure HTTP headers
- [ ] Implement rate limiting
- [ ] Use environment variables for secrets
- [ ] Keep dependencies updated
- [ ] Implement proper error handling (don't expose stack traces)
- [ ] Use secure session cookies
- [ ] Implement proper authentication and authorization
- [ ] Log security events
- [ ] Regular security audits
- [ ] Use Content Security Policy (CSP)
Security Testing
Test for Common Vulnerabilities
python
import unittest
class SecurityTestCase(unittest.TestCase):
def test_sql_injection(self):
# Test SQL injection prevention
response = self.client.post('/login', data={
'username': "admin' OR '1'='1",
'password': 'anything'
})
self.assertNotEqual(response.status_code, 200)
def test_xss(self):
# Test XSS prevention
response = self.client.post('/comment', data={
'comment': '<script>alert("XSS")</script>'
})
self.assertNotIn(b'<script>', response.data)
def test_csrf_protection(self):
# Test CSRF protection
response = self.client.post('/update-profile', data={
'email': 'new@example.com'
})
self.assertEqual(response.status_code, 400)Resources
Summary
Security should be a priority from the start of your project. Implement these practices to protect your Flask application and user data from common vulnerabilities. Remember to stay updated with the latest security advisories and regularly audit your application for potential security issues.