Overview
In CVE-2022-31177, Flask-AppBuilder prior to 4.1.3 allowed an authenticated Admin user to query users by partial password hash strings. The filtering mechanism could use LIKE queries against the password_hash field, enabling an attacker to infer partial password hashes and their associated users. The vulnerability is categorized as CWE-200 and arises from relying on sensitive data in search filters. The issue was fixed in version 4.1.3 and upgrading is advised.
Impact and exploitation: An authenticated admin could craft queries with partial fragments of salted and hashed passwords and retrieve matching user accounts. Although the API responses may not include full hashes, the presence of matches can reveal which users have particular hash fragments, enabling targeted password guessing or information leakage. The root cause is untrusted input shaping a query against a sensitive data field in an admin endpoint.
Manifestation and Flask guidance: In Flask apps that use Flask-AppBuilder, endpoints that expose user data or allow filtering by password_hash can leak sensitive information. To fix, upgrade Flask-AppBuilder to 4.1.3 or newer, and remove any filtering on password_hash. Ensure only non sensitive fields are searchable, and do not include password_hash in API responses. Implement proper role based access control and validate inputs.
Remediation approach: upgrade to 4.1.3+, audit admin endpoints, implement field whitelisting, remove password_hash exposure, restrict to safe search fields, add rate limiting, and add tests.
Affected Versions
Any version prior to 4.1.3 (e.g., 4.1.2, 4.1.1, 4.0.x).
Code Fix Example
Flask API Security Remediation
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
db = SQLAlchemy(app)
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), nullable=False)
def is_admin_user():
# Placeholder: in real apps use Flask-Login or similar
return request.args.get('admin') == '1'
# Vulnerable endpoint
@app.route('/admin/search_users')
def search_users_vuln():
if not is_admin_user():
return jsonify({'error':'forbidden'}), 403
q = request.args.get('q','')
# Vulnerable: allows filtering by parts of password hash
users = User.query.filter(User.password_hash.like(f"%{q}%")).all()
return jsonify([{'id': u.id, 'username': u.username, 'password_hash': u.password_hash} for u in users])
# Fixed endpoint
@app.route('/admin/search_users_fixed')
def search_users_fixed():
if not is_admin_user():
return jsonify({'error':'forbidden'}), 403
q = request.args.get('q','')
# Fixed: do not filter on password_hash; search only safe fields
users = User.query.filter(User.username.like(f"%{q}%")).limit(100).all()
return jsonify([{'id': u.id, 'username': u.username} for u in users])
if __name__ == '__main__':
app.run(debug=True)