
مختبر قائم على Docker لإعادة إنتاج CVE-2026-46645، وهو تجاوز تفويض في نقطة نهاية ajax_lookup الخاصة بـ SQLAdmin. يتضمن أهدافًا معرضة ومصححة، ونص PoC، وخطوات إعادة إنتاج يدوية باستخدام curl للبحث والتعليم الأمني.
يحتوي هذا المستودع على مختبر Docker محلي لإعادة إنتاج CVE-2026-46645، وهي ثغرة تجاوز تفويض تؤثر على نقطة نهاية ajax_lookup في SQLAdmin.
SQLAdmin هي واجهة إدارة لنماذج SQLAlchemy في تطبيقات Starlette وFastAPI. يحدث السلوك المعرّض للخطر عندما يقيّد تطبيق كائن ModelView باستخدام is_accessible(request)، لكن مسار ajax_lookup في SQLAdmin لا يفرض نفس قرار التحكم في الوصول قبل إرجاع نتائج البحث.
يقارن هذا المختبر بين نسختين من SQLAdmin:
| الخدمة | إصدار SQLAdmin | الغرض | URL |
|---|---|---|---|
vuln | 0.25.0 | هدف معرّض للخطر | http://127.0.0.1:8001 |
patched | 0.25.1 | هدف مقارنة بعد التصحيح | http://127.0.0.1:8002 |
سلسلة الثغرات الموضحة هي:```text Authenticated low-privileged user → restricted SQLAdmin ModelView → ModelView.is_accessible(request) returns False → user directly requests the ajax_lookup endpoint → SQLAdmin 0.25.0 returns relationship lookup data → SQLAdmin 0.25.1 blocks the same request with HTTP 403
المختبر يستخدم عمدًا نموذج بيانات بسيطًا باسم `Report` / `SecretProject` لجعل تجاوز التصريح سهل الفهم. أسماء النماذج هذه ليست السبب الجذري للثغرة. إنها تُستخدم فقط لإنشاء حالة استنساخ خاضعة للتحكم.
هذا المختبر مصمم للبحث المحلي الخاضع للتحكم، وفهم مستوى الكود المصدري، والتوضيح في ملف الأعمال فقط.
## حقائق مُتحقَّق منها
| الادعاء | الدليل | كيفية التحقق في هذا المختبر |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| نقطة نهاية `ajax_lookup` في SQLAdmin هي المكوّن المتأثر. | يصف الإشعار العام تنسيق نقطة النهاية المتأثرة بالشكل `GET /{identity}/ajax/lookup?name=<field>&term=<query>`. | شغّل الـ PoC ولاحظ الطلبات إلى `/admin/report/ajax/lookup?name=project&term=Secret`. |
| يُستخدم SQLAdmin `0.25.0` كهدف مقارنة قابل للثغرة. | يثبّت المختبر `sqladmin==0.25.0` في حاوية `vuln`. | شغّل `docker compose exec -T vuln python -m pip show sqladmin`. |
| يُستخدم SQLAdmin `0.25.1` كهدف مقارنة مُصحَّح. | يحدد الإشعار العام وملاحظات الإصدار `0.25.1` كنسخة مُصحَّحة. | شغّل `docker compose exec -T patched python -m pip show sqladmin`. |
| السبب الجذري في مسار `Admin.ajax_lookup()` العلوي من SQLAdmin. | يضيف التصحيح التحقق من المصادقة المفقود وتطبيق `is_accessible(request)` إلى `ajax_lookup()`. | افحص `Admin.ajax_lookup()` داخل الحاويتين باستخدام الأوامر في هذا README. |
| ينشئ المختبر `ModelView` مُقيَّدًا. | يُرجع `ReportAdmin.is_accessible(request)` القيمة `False` عمدًا. | افحص `app/main.py`. |
| يستخدم الـ PoC جلسة مُصادَقًا عليها. | يسجّل الـ PoC الدخول أولاً إلى `/admin/login`، ويحتفظ بملف تعريف الجلسة، ثم يطلب `ajax_lookup`. | شغّل `python3 poc/poc.py --base-url http://127.0.0.1:8001`. |
| الإشارة القابلة للثغرة هي كشف البيانات. | يُرجع SQLAdmin `0.25.0` HTTP 200 ونتائج بحث JSON من عرض مُقيَّد. | يجب أن يُرجع الهدف القابل للثغرة `Secret Project Alpha` و`Secret Project Beta`. |
| الإشارة المُصحَّحة هي رفض الوصول. | يُرجع SQLAdmin `0.25.1` HTTP 403 لنفس الطلب المُصادَق عليه. | يجب أن يُرجع الهدف المُصحَّح `403 Forbidden`. |
## الافتراضات والمجهولات
يستخدم المختبر `sqladmin==0.25.0` كخط أساس قابل للثغرة و`sqladmin==0.25.1` كخط أساس مُصحَّح.
يركّز المختبر على حالة تجاوز التفويض حيث:```text
A user is authenticated,
the target ModelView is not accessible,
but ajax_lookup is requested directly.
المختبر لا يحاول إعادة إنتاج كل نمط نشر ممكن لـ SQLAdmin. إنه ينشئ عمدًا تطبيق Starlette صغيرًا مع عرض إداري واحد مقيد بحيث يسهل التحقق من الفرق في السلوك بين الإصدارات الضعيفة والمصححة.
نموذجا Report وSecretProject كائنان خاصان بالمختبر فقط. إنهما ليسا جزءًا من SQLAdmin نفسه.
إن إثبات المفهوم (PoC) لا يحاول تصعيد الامتيازات، أو تعديل البيانات، أو سرقة الجلسات، أو إجراء استدعاءات خارجية، أو الثبات، أو شن هجمات ضد أنظمة غير تابعة للمختبر.
السبب الجذري يكمن في مسار Admin.ajax_lookup() العلوي في SQLAdmin، وليس في كود تطبيق هذا المختبر.
يتيح SQLAdmin للمطورين تقييد الوصول إلى طرق العرض الإدارية عن طريق تجاوز:```python ModelView.is_accessible(request)
من المتوقع أن تفرض مسارات الإدارة الأخرى قرار التحكم في الوصول هذا قبل السماح للطلب بالمتابعة. على سبيل المثال، تتحقق مسارات مثل list وcreate وdetails وdelete وedit وexport مما إذا كان الطلب الحالي مسموحًا له بالوصول إلى `ModelView` الهدف.
لم يفرض مسار `ajax_lookup` المعرّض للخطر نفس قرار التحكم في الوصول.
تُستخدم نقطة نهاية `ajax_lookup` بواسطة ميزة `form_ajax_refs` في SQLAdmin لتحميل قيم العلاقات ديناميكيًا. تنسيق نقطة النهاية الخاصة بها هو:```text
/admin/<identity>/ajax/lookup?name=<field>&term=<query>
في الإصدار الضعيف، تقوم ajax_lookup() بحلّ ModelView المستهدف، وتقرأ اسم حقل البحث ومصطلح البحث من سلسلة الاستعلام، ثم تستدعي محمّل AJAX وتُرجع نتائج JSON. الخطوة الأمنية المفقودة هي أنها لا تتحقق أولاً مما إذا كان الطلب الحالي مسموحًا له بالوصول إلى ModelView ذلك.
الأثر الأمني هو أن المستخدم المصادق قد يُمنع من الوصول إلى عرض إداري مقيد عبر مسارات واجهة المستخدم العادية، لكنه لا يزال يستطيع طلب نقطة نهاية AJAX الخاصة بالبحث لهذا العرض مباشرةً وتلقي بيانات البحث عن العلاقات.
يعالج SQLAdmin 0.25.1 ذلك من خلال فرض التحكم في الوصول داخل ajax_lookup(). يتحقق المسار المُصحح من model_view.is_accessible(request) ويُرجع HTTP 403 عندما لا يكون العرض المستهدف قابلاً للوصول.
يُعرّف هذا المختبر ReportAdmin.is_accessible(request) لكي يُرجع False فقط لإعادة إنتاج الحالة الضعيفة. كود المختبر ليس السبب الجذري. إنه بيئة اختبار مُتحكم بها تُثبت ما إذا كان مسار ajax_lookup() العلوي في SQLAdmin يحترم قرار التحكم في الوصول.
الفرق المتوقع في السلوك:```text sqladmin 0.25.0 -> HTTP 200 with JSON lookup results sqladmin 0.25.1 -> HTTP 403 Forbidden
## ملخص التصحيح المصدر
التصحيح الجوهري من المنبع هو إضافة فرض المصادقة والتفويض إلى `Admin.ajax_lookup()`.
السلوك المُصحَّح يعادل:```python
@login_required
async def ajax_lookup(self, request):
identity = request.path_params["identity"]
model_view = self._find_model_view(identity)
if not model_view.is_accessible(request):
raise HTTPException(status_code=403)
name = request.query_params.get("name")
term = request.query_params.get("term")
...
فحص التفويض الرئيسي هو:```python if not model_view.is_accessible(request): raise HTTPException(status_code=403)
يوضح المختبر أن هذا الفحص غير موجود في النسخة الضعيفة وموجود في النسخة المُصحَّحة.
## بنية المختبر
يشغّل المختبر تطبيقَيْ Starlette معزولَيْن عبر Docker Compose.```text
.
├── app/
│ ├── __init__.py
│ └── main.py
├── docker-compose.yml
├── patched/
│ └── Dockerfile
├── poc/
│ └── poc.py
├── README.md
├── requirements/
│ ├── patched.txt
│ └── vuln.txt
└── vuln/
└── Dockerfile
الخدمتان تشغّلان نفس كود التطبيق لكنهما يثبّتان إصدارين مختلفين من SQLAdmin: