Broken Access Control in Flask
How Broken Access Control Manifests in Flask
Broken Access Control in Flask applications often stems from improper session management and inadequate authorization checks. Flask's default session handling stores session data client-side in signed cookies, which creates specific vulnerabilities that attackers can exploit.
One common pattern involves using user IDs stored in session cookies as the sole authorization mechanism. Consider this vulnerable Flask route:
@app.route('/user//profile')
def user_profile(user_id):
# No authorization check - anyone can view any profile
user = User.query.get(user_id)
return render_template('profile.html', user=user)
An attacker can simply modify the URL parameter to access any user's profile. This is particularly dangerous in Flask because route parameters are automatically converted to Python types, making it easy to iterate through user IDs.
Flask's session management introduces another attack vector. Since sessions are signed but not encrypted by default, an attacker who obtains the secret key can modify session contents:
# Vulnerable: using session data without validation
@app.route('/admin')
def admin_panel():
if 'is_admin' in session and session['is_admin']:
return render_template('admin.html')
return redirect('/login')
If the secret key is exposed through source code repositories or configuration files, an attacker can forge admin sessions. Flask's Werkzeug debugger, if left enabled in production, can also expose the secret key through stack traces.
Property-level authorization bypasses are another Flask-specific issue. When using SQLAlchemy with Flask, developers often forget to check object ownership:
@app.route('/api/items/', methods=['PUT'])
def update_item(item_id):
data = request.json
item = Item.query.get(item_id)
# No check that current_user owns this item
item.name = data['name']
item.save()
return jsonify(item.to_dict())
This allows any authenticated user to modify any item in the database. The vulnerability is exacerbated in Flask because the ORM layer doesn't enforce authorization by default.
Flask-Specific Detection
Detecting Broken Access Control in Flask requires examining both the application code and runtime behavior. middleBrick's black-box scanning approach is particularly effective for Flask applications because it tests the actual API surface without requiring source code access.
For Flask applications, middleBrick automatically tests for several Flask-specific vulnerabilities:
- Session manipulation: Testing whether session-based authorization can be bypassed by modifying cookie values
- Route parameter tampering: Iterating through numeric IDs to detect IDOR vulnerabilities in Flask's automatic type conversion
- Method-based access control: Testing if HTTP method restrictions are properly enforced
- Property authorization: Attempting to access or modify objects that should be restricted to owners
The CLI tool makes it easy to scan Flask APIs:
npm install -g middlebrick
middlebrick scan https://api.example.com --api-name "Flask API" --output json
This command runs all 12 security checks, including the BOLA (Broken Object Level Authorization) test that's particularly relevant for Flask applications using SQLAlchemy or similar ORMs.
For development teams, the GitHub Action provides continuous monitoring:
- name: Run middleBrick Security Scan
uses: middlebrick/middlebrick-action@v1
with:
target-url: http://localhost:5000
fail-on-severity: high
token: ${{ secrets.MIDDLEBRICK_TOKEN }}
This integrates API security testing directly into Flask development workflows, catching authorization issues before they reach production.
Flask-Specific Remediation
Flask provides several native mechanisms for implementing proper access control. The most effective approach combines Flask-Login for session management with custom authorization decorators.
First, implement proper session security:
from flask import Flask, session, redirect, url_for
from flask_login import LoginManager, UserMixin, login_user, login_required, current_user
import os
app = Flask(__name__)
app.secret_key = os.environ.get('FLASK_SECRET_KEY') or os.urandom(24)
login_manager = LoginManager()
login_manager.init_app(app)
class User(UserMixin):
def __init__(self, id, username, is_admin=False):
self.id = id
self.username = username
self.is_admin = is_admin
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
Next, create reusable authorization decorators for Flask routes:
from functools import wraps
def role_required(role):
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
if not current_user.is_authenticated:
return redirect(url_for('login'))
if not hasattr(current_user, 'role') or current_user.role != role:
return "Insufficient permissions", 403
return fn(*args, **kwargs)
return decorated_view
return wrapper
def owner_required(model_class):
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
resource_id = kwargs.get('id')
resource = model_class.query.get(resource_id)
if not resource or resource.owner_id != current_user.id:
return "Access denied", 403
return fn(*args, **kwargs)
return decorated_view
return wrapper
Apply these decorators to Flask routes:
@app.route('/admin/dashboard')
@login_required
@role_required('admin')
def admin_dashboard():
return render_template('admin.html')
@app.route('/items/', methods=['GET', 'PUT', 'DELETE'])
@login_required
@owner_required(Item)
def manage_item(id):
if request.method == 'GET':
return jsonify(ItemSchema().dump(item))
elif request.method == 'PUT':
# Update logic here
pass
elif request.method == 'DELETE':
# Delete logic here
pass
For property-level authorization in Flask-SQLAlchemy models, implement ownership checks at the model level:
class Item(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
owner_id = db.Column(db.Integer, db.ForeignKey('user.id'))
def can_be_accessed_by(self, user):
return self.owner_id == user.id or user.is_admin
@staticmethod
def get_by_id_with_auth(item_id, user):
item = Item.query.get(item_id)
if not item or not item.can_be_accessed_by(user):
raise PermissionError("Access denied")
return item