
Доказательство концепции для CVE-2025-23211; воспроизводит уязвимость, связанную с конфиденциальностью данных, для поддержки авторизованной проверки безопасности, тестирования исправлений и проверки мер по устранению уязвимости.
Самодостаточное воспроизводимое окружение, демонстрирующее CVE-2025-23211 — уязвимость server-side template injection (SSTI) в Tandoor Recipes, приводящую к удалённому выполнению кода. Репозиторий разворачивает уязвимую версию, показывает эксплойт, затем разворачивает исправленную версию и показывает, как тот же ввод отклоняется.
| CVE | CVE-2025-23211 |
| Advisory | GHSA-r6rj-h75w-vj8v |
| Class | Server-Side Template Injection (Jinja2), CWE-94 / CWE-1336 |
| CVSS 3.1 | 9.9 (Critical) - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| Affected | Tandoor Recipes <= 1.5.23 |
| Fixed in | 1.5.24 |
Tandoor Recipes — это открытое self-hosted приложение для планирования питания и управления рецептами. Бэкенд — Django (Python), обслуживаемый Gunicorn, фронтенд — одностраничное приложение на Vue.js, данные хранятся в PostgreSQL.
Tandoor позволяет пользователям писать выражения шаблонов Jinja2 внутри текста инструкций рецепта — функция, предназначенная для таких задач, как масштабирование количества ингредиентов. Эти инструкции рендерятся на сервере.
Текст инструкций обрабатывается функцией render_instructions() в
cookbook/helper/template_helper.py. В уязвимых версиях текст передаётся в
стандартный Template() Jinja2 и рендерится без песочницы:
template = Template(instructions)
instructions = template.render(ingredients=ingredients, scale=scale)
Поскольку стандартное окружение Jinja2 предоставляет доступ к графу объектов
Python, аутентифицированный пользователь, имеющий право редактировать рецепт,
может внедрить выражение шаблона, которое проходит от пустого кортежа до
subprocess.Popen и выполняет произвольную команду на сервере. Контейнер по
умолчанию работает от root, поэтому команда выполняется от root.
Инструкции проходят через этап Markdown перед тем, как Jinja2 их отрендерит,
и этот этап удаляет неэкранированные символы подчёркивания. Поэтому эксплойт
кодирует подчёркивания в hex (\x5f\x5f) и использует фильтр attr() вместо
точечной нотации. См. payloads.txt.
compose-vulnerable.yml Tandoor 1.5.23 (vulnerable) plus PostgreSQL
compose-patched.yml Tandoor 1.5.24 (patched) plus PostgreSQL
payloads.txt The two proof-of-concept payloads and expected output
enumerate-popen.txt Helper for finding the subprocess.Popen index on other images
Доказательства в этом репозитории были получены на конфигурации, описанной ниже. Любой свежий релиз Docker с Compose v2 или новее воспроизводит тот же результат.
Запустите уязвимый стек:
docker compose -f compose-vulnerable.yml up -d
Дождитесь, пока в логах веб-контейнера появится сообщение о запуске воркера,
затем откройте http://localhost:8080.
Зарегистрируйте первую учётную запись. Первый пользователь становится суперпользователем. При запросе задайте имя Space.
Создайте новый рецепт. В поле Steps > Instructions вставьте Payload 1 из
payloads.txt, затем сохраните.
Откройте рецепт для просмотра. Отрендеренные инструкции показывают
<class 'subprocess.Popen'>, что подтверждает, что инъекция достигает
внутренностей Python.
Повторите с Payload 2. Просмотр рецепта теперь выполняет whoami и
отображает root.
Подтвердите пользователя процесса напрямую:
docker compose -f compose-vulnerable.yml exec web_recipes whoami
Остановите уязвимый стек:
docker compose -f compose-vulnerable.yml down
Запустите исправленный стек:
docker compose -f compose-patched.yml up -d
Повторите те же шаги с теми же payload'ами. Вместо выполнения просмотр
рецепта теперь показывает Could not parse template code. Error: Undefined Error.
Чтобы сбросить всё и начать с пустой базы данных, выполните
docker compose -f compose-vulnerable.yml down -v. Флаг -v удаляет
именованные тома, поэтому при следующем запуске база данных создаётся с нуля.
Патч (коммит e6087d5) заменяет небезопасный Template() на
SandboxedEnvironment из Jinja2:
env = SandboxedEnvironment()
instructions = env.from_string(instructions).render(ingredients=ingredients, scale=scale)
Песочница переопределяет поиск атрибутов в Jinja2, запрещая доступ к небезопасным
атрибутам, таким как __class__, __base__, __subclasses__ и __getitem__.
Без них у payload'а нет пути от обычного объекта до subprocess.Popen, поэтому
выражение завершается ошибкой вместо выполнения.
Эксплойт обращается к атрибутам через фильтр attr(), а не через точечную
нотацию, поэтому в исправленном коде заблокированный доступ проявляется как
UndefinedError, а не как SecurityError; оба обрабатываются версией с
песочницей.
Этот репозиторий воспроизводит публично раскрытую и уже исправленную уязвимость в образовательных целях. Запускайте его только против локальных контейнеров, предоставленных здесь. Не атакуйте системы, которыми вы не владеете и на тестирование которых у вас нет разрешения.