Flask-AppBuilder, Access Control Bypass, CVE-2021-32809 (Medium)

Listen to this Post

How the CVE works:

The ajax_lookup endpoint in application.py handles GET requests for autocomplete lookups on model fields. Unlike all other endpoints (list, create, edit, delete, details, export), it originally lacked two critical security checks. First, it had no @login_required decorator, allowing unauthenticated access. Second, it bypassed the is_accessible() method which developers override to implement per-model access control. An authenticated but unauthorized user could send a crafted GET request to /{identity}/ajax/lookup?name=&term=. The endpoint would execute a database query on the restricted model and return matching records as JSON. This happens because the endpoint directly calls the model’s query interface without verifying if the current user has permission to access that model. The is_accessible() check, which all other endpoints enforce via a decorator or manual call, was completely absent. Therefore, any restriction placed on a model by overriding is_accessible() (e.g., hiding sensitive tables from certain roles) is silently ignored. The fix added both the standard @login_required decorator and an explicit is_accessible(request) check that raises HTTP 403 when False. Before the fix, even unauthenticated users could exploit this (addressed in 1035), but the authenticated bypass remained as the final gap.

DailyCVE Form:

Platform: Flask-AppBuilder
Version: <3.3.4
Vulnerability: Access control bypass
Severity: Medium
date: 2025-05-21

Prediction: 2021-07-30

What Undercode Say:

Check if endpoint is vulnerable
curl -X GET "http://target.example.com/users/ajax/lookup?name=username&term=admin" -H "Cookie: session=..."
Simulate is_accessible() bypass using authenticated session
python3 -c "import requests; s=requests.Session(); s.post('http://target/login', data={'username':'user','password':'pass'}); r=s.get('http://target/restricted_model/ajax/lookup?name=id&term=1'); print(r.text)"
Code snippet of vulnerable endpoint (before fix)
def ajax_lookup(self):
term = request.args.get('term')
field = request.args.get('name')
model = self.get_model()
query = model.query.filter(getattr(model, field).like(f'%{term}%'))
return jsonify([{'id': r.id, 'value': getattr(r, field)} for r in query.limit(20)])

Exploit:

Send GET request with authenticated session cookie to /{identity}/ajax/lookup?name=any_field&term=% . No special headers needed. If response returns JSON rows from a restricted model, the CVE is present. Example: /admin/ajax/lookup?name=email&term=@ . For models where is_accessible() returns False for the current user, the exploit still returns data.

Protection from this CVE:

Upgrade to Flask-AppBuilder >= 3.3.4. If patching is impossible, manually override the ajax_lookup method in all ModelViews to call self.is_accessible() and raise HTTP 403 on failure, plus require @login_required. Alternatively, disable the ajax_lookup feature entirely by setting `ajax_lookup = False` in the ModelView class. Monitor logs for unexpected GET requests containing `/ajax/lookup` with unusual `name` parameters.

Impact:

Authenticated users can read any model field exposed via a ModelView regardless of custom is_accessible() restrictions. This leads to data leakage of sensitive records (e.g., user emails, internal IDs, or confidential attributes). Unauthenticated users were also exposed before 1035, but after that fix only authenticated users retain the bypass. Developers relying solely on is_accessible() for row-level or model-level access control remain unprotected unless this endpoint is fixed.

🎯Let’s Practice Exploiting & Learn Patching For Free:

Sources:

Reported By: github.com
Extra Source Hub:
Undercode

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow DailyCVE & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin Featured Image

Scroll to Top