
يكتشف مسارات API التي تحمل تفويضًا أضعف من نظيراتها. استعاد CVE-2026-45316 من المصدر. يتضمن النتائج السلبية.
تكتشف مسارات API التي تحمل تحكمًا في التفويض أضعف من مساراتها الشقيقة.
عثرت على CVE-2026-45316 في Open WebUI من الكود المصدري وحده، دون أي معرفة مسبقة بنشرة أمنية:
[MISMATCH] POST /notes/{id}/pin (notes.py:pin_note_by_id)
3/3 comparable write operations on note require has_access(write);
this route requires only has_access(read)
POST /notes/{id}/update has_access(write)
POST /notes/{id}/access/update has_access(write)
DELETE /notes/{id}/delete has_access(write)
عبر 2,568 مسارًا في خمس قواعد أكواد إنتاجية أسفرت عن 4 اكتشافات. كان اثنان منها حقيقيين. يشرح ملف README هذا الرقمين.
فئة كبيرة من أخطاء التفويض ليست فحصًا معطوبًا، بل فحصًا مفقودًا أو مضعَّفًا على مسارٍ أصابت فيه جميع مساراته الشقيقة.
أذنت Portainer لأربع نقاط نهاية شقيقة للقوالب ولم تأذن للخامسة. قيّد Signal K معدل تسجيل دخول HTTP، لكن ليس معدل تسجيل دخول WebSocket. مسار تثبيت الملاحظات في Open WebUI عدّل ملاحظة أثناء التحقق من إذن القراءة، بينما كانت كل عمليات تغيير الملاحظات الأخرى تتحقق من إذن الكتابة.
دائمًا بالشكل نفسه:
✓ ✓ ✓ ✓ ✗
يحتوي المستودع أصلًا على القاعدة. خالفها مسار واحد. لذا أعد بناء القاعدة من الكود وأبلغ عن الاستثناء. لا ملف سياسات، لا إعدادات، لا تعليقات توضيحية. مجموعة الإثبات هي المسارات الشقيقة نفسها.
python3 enforcement_coverage.py /path/to/repo
python3 enforcement_coverage.py /path/to/repo --density
python3 enforcement_coverage.py /path/to/repo --json
يتطلب Python 3.10+، بلا تبعيات. يدعم FastAPI فقط.
الأحكام هي MISSING وMISMATCH وPRESERVED وUNKNOWN. تمتنع UNKNOWN
عن الحكم ولا تُبلَّغ أبدًا.
الاستخراج. يستخرج عناصر التحكم من توقيع Depends()، ومن مُزيِّن
dependencies=[]، ومن التبعيات على مستوى الموجّه، ومن أجسام الدوال،
ومن الدوال الغلافية، ومن قوائم أصناف الأذونات.
استخراج أجسام الدوال أساسي. في Open WebUI، يحمل 216 من أصل 608 معالجاتٍ الفحص الحاسم داخل الدالة:
if user.role != 'admin' and not await AccessGrants.has_access(
user_id=user.id, resource_type='note',
resource_id=note.id, permission='write', db=db,
):
raise HTTPException(status_code=403)
صنف العملية يُستمد مما يفعله المعالج بالمورد، لا من فعل HTTP.
POST /notes/{id}/chat هي عملية POST تقرأ ملاحظة.
تصنيف المفردات. لا تشكّل قيمتان داخل عائلة تحكم واحدة مقياس قوة بالضرورة:
has_access {read, write} LEVEL
ensure_flow_permission {create,delete,execute,read,write} ACTION
has_permission {features.notes, workspace.tools} SCOPE
تدعم مفردات LEVEL فقط مقارنة القوة. أدت مقارنة FlowAction.CREATE مع
WRITE السائدة إلى ست نتائج إيجابية خاطئة في Langflow قبل إصلاح
هذه المشكلة.
مرشح الاتجاه. لا تُبلَّغ سوى الانحرافات نحو تحكم أقل تقييدًا. الأشد من السابقة ليس ثغرة.
نتيجتان إيجابيتان حقيقتان.
POST /notes/{id}/pin في Open WebUI — CVE-2026-45316.
اكتشاف إضافي هو تباين في الأذونات في Netflix Dispatch:
يستخدم POST /{incident_id}/resources إذن IncidentViewPermission بينما تستخدم
ست عمليات كتابة شقيقة إذن IncidentEditPermission.
يُرجع IncidentViewPermission القيمة True لأي حادثة غير مقيدة؛
ويتطلب IncidentEditPermission دور مسؤول أو قائد أو مُبلِّغ.
يُدرج المعالج إنشاء التذاكر والمجموعات في قائمة الانتظار دون أي فحص إضافي.
لم يُبلَّغ عنه لأنه لا يوجد مكان للإبلاغ: أرشفت Netflix المستودع في 3 سبتمبر 2025 وهو للقراءة فقط، ولا يحتوي على ملف SECURITY.md، والإبلاغ الخاص عن الثغرات معطّل للمستودعات المؤرشفة، ومشروع Dispatch مُدرج صراحةً خارج نطاق برنامج مكافآت الثغرات لدى Netflix. النشر هنا هو قناة الإفصاح الوحيدة المتبقية. إنها منخفضة الخطورة — تتطلب عضوًا موثّقًا في المؤسسة ولا تؤثر إلا على الحوادث غير المقيدة — والمشروع لم يعد قيد الصيانة.
نتيجتان إيجابيتان خاطئتان. يكتب POST /tools/{id}/valves/user/update
إعدادات الصمامات الخاصة بالمستخدم ويحتاج بشكل مشروع إلى إذن قراءة فقط على
الأداة — هدف التغيير كيان مختلف عن موضوع التفويض. وكذلك مسار قاعدة معرفة
في Langflow حيث الحارس الشقيق غير مطلوب.
هذا هو الجزء المفيد.
تمتلك Danswer تغطية تحكم بنسبة 95.9% وأنتجت صفر اكتشافات مع
نسبة UNKNOWN بلغت 80.7%. مفرداتها هي require_permission('basic_access')
و('manage_connectors') — فضاء قدرات (namespace)، وليس مقياس قوة.
لا يمكنك القول إن manage_connectors أضعف من read_connectors.
ما يتنبأ بالاكتشافات هو استدعاء إذن مقيّد بالموارد مع وسيط قوة
مرتب، مثل has_access(resource, read|write). كانت واحدة من كل
خمس قواعد أكواد تمتلك ذلك.
خمسة أنماط عبر خمسة مستودعات. تطلب كلٌّ منها عملًا على أداة الاستخراج قبل أن يتمكن التحليل من العمل أصلًا.
في أي مستودع وتحت أي إعداد. معظم المسارات لا تنتمي إلى عائلة شقيقة من ثلاثة مسارات أو أكثر ذات تحكم متناسق.
لم يكن أيٌّ من هذه متوقعًا مسبقًا.
Depends()العيب 13 هو المثير للاهتمام. مسارات اقتراح الوسوم في Dispatch تفتقر إلى
CaseViewPermission الذي تحمله مساراتها الشقيقة. لكن ذلك الإذن يُرجع
True لأي حالة غير مقيدة، وتتحقق الخدمة أصلًا من visibility == restricted
مباشرةً داخل الكود. المساران متكافئان. يتطلب اكتشاف ذلك تحليل تكافؤ
دلالي، لا تحليلًا بنيويًا.
الآلية تعمل. استرجعت CVE منشورة وفجوة تفويض واحدة غير مُبلَّغ عنها من الكود المصدري، مع مجموعات إثبات قابلة للتدقيق، مستخدمةً فقط معلومات كانت متاحة قبل دمج الالتزام الهش.
العائد قرابة اكتشاف واحد لكل 1,000 مسار، ويتطلب بنية كانت موجودة في واحدة من خمس قواعد أكواد كبيرة.
مفيدة كأداة تدقيق لقاعدة أكواد تمتلك مفردات أذونات مقيّدة بالموارد. وليست، استنادًا إلى هذا الدليل، ماسحًا للأغراض العامة.
يعود الكشف الساكن عن ثغرات التحكم في الوصول عبر استنتاج الافتراضات الضمنية إلى USENIX Security 2011. استخرج ACMiner فحوصات التفويض في البرمجيات الوسيطة لنظام أندرويد. يوفّر Semgrep كشفًا مدعومًا بالذكاء الاصطناعي يستهدف التفويض المفقود، وأبلغ عن دقة 61% في تقييم أحد العملاء. تنشر OWASP ورقة غش (cheat sheet) لاختبار انحدار التفويض، والمنهج الموصى به فيها هو صيانة مصفوفة الفاعل × المورد × الإجراء يدويًا.
هذه مقاربة ضيقة وحتمية: استرجاع ما تثبته المسارات الشقيقة فقط، والامتناع عن الحكم فيما عداه.
مرخَّصة بموجب MIT.
| المستودع | المسارات | الخاضعة للتحكم | الاكتشافات |
|---|
| LiteLLM | 808 | 87.1% | 0 |
| Danswer / Onyx | 653 | 95.9% | 0 |
| Open WebUI | 529 | 97.2% | 2 |
| Netflix Dispatch | 291 | 36.1% | 1 |
| Langflow | 287 | 47.4% | 1 |
| المستودع | النمط |
|---|
| Open WebUI | AccessGrants.has_access(resource_type=, permission=) |
| Langflow | ensure_<resource>_permission(user, Action.X) |
| LiteLLM | مقارنة الدور على user_api_key_dict.user_role |
| Netflix Dispatch | Depends(PermissionsDependency([CaseEditPermission])) |
| Danswer | require_permission('basic_access') |