
Prueba de concepto para CVE-2025-23211; reproduce la vulnerabilidad relacionada con la privacidad de datos para respaldar la validación de seguridad autorizada, las pruebas de parches y la verificación de mitigaciones.
Un entorno autocontenido y reproducible que demuestra CVE-2025-23211, una vulnerabilidad de inyección de plantillas del lado del servidor (SSTI) en Tandoor Recipes que conduce a la ejecución remota de código. El repositorio levanta la versión vulnerable, muestra el exploit, luego levanta la versión parcheada y muestra la misma entrada siendo rechazada.
| CVE | CVE-2025-23211 |
| Aviso | GHSA-r6rj-h75w-vj8v |
| Clase | Inyección de plantillas del lado del servidor (Jinja2), CWE-94 / CWE-1336 |
| CVSS 3.1 | 9.9 (Crítico) - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| Afecta a | Tandoor Recipes <= 1.5.23 |
| Corregido en | 1.5.24 |
Tandoor Recipes es una aplicación de código abierto, autoalojada, de planificación de comidas y gestión de recetas. El backend es Django (Python) servido por Gunicorn, el frontend es una aplicación de página única en Vue.js, y los datos se almacenan en PostgreSQL.
Tandoor permite a los usuarios escribir expresiones de plantilla Jinja2 dentro del texto de instrucciones de una receta, una funcionalidad pensada para tareas como el escalado de cantidades de ingredientes. Esas instrucciones se renderizan en el servidor.
El texto de las instrucciones es procesado por render_instructions() en
cookbook/helper/template_helper.py. En las versiones afectadas, el texto se pasa al Template() por defecto de Jinja2 y se renderiza sin un sandbox:
template = Template(instructions)
instructions = template.render(ingredients=ingredients, scale=scale)
Debido a que el entorno Jinja2 por defecto expone el grafo de objetos de Python, un usuario autenticado que puede editar una receta puede inyectar una expresión de plantilla que recorre desde una tupla vacía hasta subprocess.Popen y ejecuta un comando arbitrario en el servidor. El contenedor por defecto se ejecuta como root, por lo que el comando se ejecuta como root.
Las instrucciones pasan por un paso de Markdown antes de que Jinja2 las renderice, y ese paso elimina los guiones bajos sin procesar. Por lo tanto, el exploit codifica en hexadecimal los guiones bajos (\x5f\x5f) y utiliza el filtro attr() en lugar de la notación de punto. Véase payloads.txt.
compose-vulnerable.yml Tandoor 1.5.23 (vulnerable) más PostgreSQL
compose-patched.yml Tandoor 1.5.24 (parcheado) más PostgreSQL
payloads.txt Los dos payloads de prueba de concepto y la salida esperada
enumerate-popen.txt Ayudante para encontrar el índice de subprocess.Popen en otras imágenes
La evidencia en este repositorio fue capturada en la configuración que se muestra a continuación. Cualquier versión reciente de Docker con Compose v2 o posterior reproduce el mismo resultado.
Inicie el stack vulnerable:
docker compose -f compose-vulnerable.yml up -d
Espere hasta que los registros del contenedor web muestren un worker iniciándose, luego abra
http://localhost:8080.
Registre la primera cuenta. El primer usuario se convierte en el superusuario. Establezca un nombre de Space cuando se le solicite.
Cree una nueva receta. En el campo Steps > Instructions, pegue el Payload 1 de
payloads.txt, luego guarde.
Abra la receta para verla. Las instrucciones renderizadas muestran
<class 'subprocess.Popen'>, lo que confirma que la inyección alcanza las entrañas de Python.
Repita con el Payload 2. Ver la receta ahora ejecuta whoami y muestra root.
Confirme el usuario del proceso directamente:
docker compose -f compose-vulnerable.yml exec web_recipes whoami
Detenga el stack vulnerable:
docker compose -f compose-vulnerable.yml down
Inicie el stack parcheado:
docker compose -f compose-patched.yml up -d
Repita los mismos pasos con los mismos payloads. En lugar de ejecutarse, la vista de la receta
ahora muestra Could not parse template code. Error: Undefined Error.
Para restablecer todo y comenzar desde una base de datos vacía, ejecute
docker compose -f compose-vulnerable.yml down -v. La opción -v elimina los
volúmenes con nombre, por lo que la siguiente ejecución recrea la base de datos desde cero.
El parche (commit e6087d5) reemplaza el Template() sin sandbox con el
SandboxedEnvironment de Jinja2:
env = SandboxedEnvironment()
instructions = env.from_string(instructions).render(ingredients=ingredients, scale=scale)
El sandbox anula la búsqueda de atributos de Jinja2 para prohibir el acceso a atributos
inseguros como __class__, __base__, __subclasses__ y __getitem__.
Sin ellos, el payload no tiene ruta desde un objeto simple hasta subprocess.Popen,
por lo que la expresión falla en lugar de ejecutarse.
El exploit alcanza los atributos a través del filtro attr() en lugar de la notación de punto,
por lo que en el código parcheado el acceso bloqueado se manifiesta como un UndefinedError
en lugar de un SecurityError; ambos son manejados por la versión con sandbox.
Este repositorio reproduce una vulnerabilidad divulgada públicamente y ya parcheada con fines educativos. Ejecútelo solo contra los contenedores locales proporcionados aquí. No ataque sistemas que no posee o que no está autorizado a probar.