Broken Object Property Level Authorization

Broken Object Property Level Authorization in Flask [CVE-2022-31177]

[Updated Mar 2026] Updated CVE-2022-31177

Overview

The CVE-2022-31177 vulnerability concerns Flask-AppBuilder, a framework built on Flask. In versions prior to 4.1.3, an authenticated Admin could query other users by partial hashed password strings. While the response would not include the full hashes, attackers could infer partial password hashes and associate them with specific user accounts, enabling targeted credential research and information disclosure (CWE-200). This type of information exposure arises from weak object-property level authorization controls that allow filtering or returning sensitive fields such as password_hash through admin endpoints. The impact can be severe in apps where admin users manage user records and where password hashes might be exposed to unauthorized observers. Exploitation typically occurs when an admin-facing API endpoint or UI allows filtering or returning user data based on the password_hash field. An attacker could pass a partial hash or rely on auto-suggested queries to enumerate users whose hashes match that substring. Even though the full hash is not disclosed, deducing partial hashes can aid offline analysis or credential stuffing against weakly protected accounts. This vulnerability demonstrates improper object-level access control and insufficient data minimization: sensitive fields like password_hash should never be exposed or used as search criteria in admin APIs. The vulnerability is categorized under CWE-200 (Information Exposure) and is specifically fixed in Flask-AppBuilder 4.1.3 (CVE-2022-31177).

Affected Versions

Flask-AppBuilder < 4.1.3

Code Fix Example

Flask API Security Remediation
from flask import Flask, request, jsonify, abort
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import or_

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
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)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(128), nullable=False)

    def to_public_dict(self):
        return {'id': self.id, 'username': self.username, 'email': self.email}

class CurrentUser:
    def __init__(self, is_admin=False):
        self.is_admin = is_admin
    @property
    def is_authenticated(self):
        return True

# Simple admin user for demonstration; in real apps integrate Flask-Login or similar
current_user = CurrentUser(is_admin=True)

# Vulnerable endpoint (do not expose password_hash)
@app.route('/admin/users/search')
def search_users_vuln():
    if not current_user.is_authenticated or not current_user.is_admin:
        abort(403)
    q = request.args.get('q')
    if q:
        # Vulnerable: filters on password_hash
        users = User.query.filter(User.password_hash.like('%' + q + '%')).all()
    else:
        users = User.query.all()
    return jsonify([u.to_public_dict() for u in users])

# Fixed endpoint (no filtering on password_hash, safe serialization)
@app.route('/admin/users/search-fixed')
def search_users_fix():
    if not current_user.is_authenticated or not current_user.is_admin:
        abort(403)
    q = request.args.get('q')
    if q:
        users = User.query.filter(or_(User.username.ilike('%' + q + '%'), User.email.ilike('%' + q + '%'))).all()
    else:
        users = User.query.all()
    return jsonify([u.to_public_dict() for u in users])

if __name__ == '__main__':
    db.create_all()
    if not User.query.first():
        u1 = User(username='admin', email='[email protected]', password_hash='hash1')
        u2 = User(username='alice', email='[email protected]', password_hash='hash2')
        db.session.add_all([u1, u2])
        db.session.commit()
    app.run(debug=True)

CVE References

Choose which optional cookies to allow. You can change this any time.