Insecure Design in Flask
How Insecure Design Manifests in Flask
Insecure design in Flask applications often stems from architectural decisions made during development that create security gaps. Unlike implementation flaws, these are fundamental design problems that persist regardless of how well the code is written.
One common pattern is improper session management. Flask's default session implementation uses client-side signed cookies, which means sensitive data should never be stored in sessions. Consider this flawed design:
# INSECURE: Storing sensitive data in client-side session
from flask import Flask, session
app = Flask(__name__)
app.secret_key = 'supersecretkey'
@app.route('/login')
def login():
session['user_id'] = user.id
session['is_admin'] = user.is_admin # Sensitive data exposed to client
session['internal_notes'] = user.notes # PII exposed
The design flaw here is treating Flask's session like a server-side store. Since sessions are signed but not encrypted by default, any user can decode and read their session contents using tools like itsdangerous.
Mass assignment vulnerabilities are another Flask-specific design issue. When using ORMs like SQLAlchemy with Flask, developers often create endpoints that accept arbitrary JSON objects and directly map them to database models:
# INSECURE: Mass assignment vulnerability
from flask import request
from models import User
@app.route('/update_profile', methods=['POST'])
def update_profile():
data = request.json
user = User.query.get(session['user_id'])
user.update(data) # Allows updating ANY field
db.session.commit()
return {'status': 'success'}
This design allows attackers to modify fields they shouldn't access, such as is_admin, account_balance, or subscription_tier.
Insecure direct object references (IDOR) frequently appear in Flask routes that use predictable identifiers. A common Flask pattern uses sequential IDs in URLs:
# INSECURE: Predictable resource identifiers
@app.route('/user/<int:user_id>')
def get_user(user_id):
user = User.query.get(user_id) # No authorization check
return jsonify(user.to_dict())
Combined with the mass assignment issue above, this creates a perfect storm where attackers can enumerate user IDs and modify any user's data.
Flask-Specific Detection
Detecting insecure design in Flask requires both static analysis and dynamic testing. For static analysis, tools like bandit can catch some implementation issues, but design flaws require deeper inspection.
middleBrick's approach to detecting Flask-specific insecure design includes:
- Session analysis: Scanning for session data that contains sensitive fields, using regex patterns to identify common sensitive keys like
is_admin,role,permissions,internal_notes - Endpoint fingerprinting: Identifying REST patterns that suggest mass assignment vulnerabilities by analyzing request schemas and response structures
- Authorization bypass testing: Attempting to access resources with modified identifiers to test for IDOR vulnerabilities
- CSRF token analysis: Checking if state-changing endpoints lack proper CSRF protection, which is a design flaw in Flask applications using cookies for auth
Here's how you might use middleBrick to scan a Flask API:
# Scan a Flask API endpoint
middlebrick scan https://api.example.com --output json
# Scan with specific focus on authorization issues
middlebrick scan https://api.example.com --category authorization --output json
# Integrate into CI/CD for Flask apps
middlebrick scan https://staging.example.com --threshold B --fail-below C
The scanner specifically looks for Flask patterns like:
- Use of
sessionwithout encryption for sensitive data - Routes with
<int:id>parameters that return user data without authorization - POST/PUT endpoints that accept JSON and directly update models
- Missing
@login_requiredor equivalent decorators on sensitive routes
For runtime detection, Flask's built-in debugging features can help identify design issues during development. Enabling PROPAGATE_EXCEPTIONS and using Flask's test client can reveal authorization bypasses:
# Test for IDOR vulnerabilities
with app.test_client() as client:
# Authenticate as user 1
client.post('/login', json={'username': 'user1', 'password': 'pass'})
# Try to access user 2's data
response = client.get('/user/2')
assert response.status_code == 403, "IDOR vulnerability detected"
Flask-Specific Remediation
Remediating insecure design in Flask requires architectural changes rather than just code fixes. Here are Flask-specific solutions:
Secure session management starts with proper configuration:
from flask import Flask, session
from flask.sessions import SecureCookieSessionInterface
from cryptography.fernet import Fernet
app = Flask(__name__)
app.secret_key = Fernet.generate_key().decode() # Strong random key
# Custom session interface for server-side sessions
class ServerSideSessionInterface(SecureCookieSessionInterface):
def open_session(self, app, request):
return {} # Return empty session, store data server-side
app.session_interface = ServerSideSessionInterface()
# Store session data server-side using Redis
import redis
redis_client = redis.Redis(host='localhost', port=6379)
@app.route('/login')
def login():
session_id = str(uuid.uuid4())
redis_client.setex(f"session:{session_id}",
timedelta(hours=1),
json.dumps({'user_id': user.id}))
response = jsonify({'message': 'Logged in'})
response.set_cookie('session_id', session_id, httponly=True, secure=True)
return response
Preventing mass assignment requires explicit field whitelisting:
from flask import request
from marshmallow import Schema, fields, validate
class UserProfileSchema(Schema):
email = fields.Email(required=True)
display_name = fields.Str(required=True, validate=validate.Length(max=50))
# Only allow specific fields
@app.route('/update_profile', methods=['PUT'])
def update_profile():
schema = UserProfileSchema()
try:
data = schema.load(request.json)
except ValidationError as err:
return jsonify(err.messages), 400
user = User.query.get(session['user_id'])
for key, value in data.items():
setattr(user, key, value) # Only allowed fields can be set
db.session.commit()
return jsonify({'status': 'success'})
Authorization middleware helps prevent IDOR across all routes:
from functools import wraps
def require_ownership(resource_field='user_id'):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
resource_id = kwargs.get(resource_field)
if not resource_id:
return jsonify({'error': 'Resource ID required'}), 400
# Check if current user owns this resource
resource = f.__globals__[f.__name__].view_class.model.query.get(resource_id)
if not resource or resource.user_id != session['user_id']:
return jsonify({'error': 'Access denied'}), 403
return f(*args, **kwargs)
return decorated_function
return decorator
# Usage
@app.route('/user/<int:user_id>')
@require_ownership()
def get_user(user_id):
user = User.query.get(user_id)
return jsonify(user.to_dict())
CSRF protection is essential for Flask apps using cookies:
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
# For APIs, use double submit cookie or custom headers
@app.after_request
def add_csrf_token(response):
if request.method in ['POST', 'PUT', 'DELETE']:
response.set_cookie('X-CSRF-Token', generate_csrf_token())
return response