
Preuve de concept pour CVE-2025-23211 ; reproduit la vulnérabilité liée à la confidentialité des données afin de prendre en charge la validation de sécurité autorisée, les tests de correctifs et la vérification des mesures d'atténuation.
Un environnement autonome et reproductible démontrant CVE-2025-23211, une vulnérabilité d'injection de modèle côté serveur (SSTI) dans Tandoor Recipes qui mène à l'exécution de code à distance. Le dépôt déploie la version vulnérable, montre l'exploit, puis déploie la version corrigée et montre la même entrée rejetée.
| CVE | CVE-2025-23211 |
| Avis | GHSA-r6rj-h75w-vj8v |
| Classe | Injection de modèle côté serveur (Jinja2), CWE-94 / CWE-1336 |
| CVSS 3.1 | 9.9 (Critique) - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| Affecté | Tandoor Recipes <= 1.5.23 |
| Corrigé dans | 1.5.24 |
Tandoor Recipes est une application open-source auto-hébergée de planification de repas et de gestion de recettes. Le backend est Django (Python) servi par Gunicorn, le frontend est une application monopage Vue.js, et les données sont stockées dans PostgreSQL.
Tandoor permet aux utilisateurs d'écrire des expressions de modèle Jinja2 dans le texte des instructions d'une recette, une fonctionnalité destinée à des tâches telles que la mise à l'échelle des quantités d'ingrédients. Ces instructions sont rendues sur le serveur.
Le texte des instructions est traité par render_instructions() dans
cookbook/helper/template_helper.py. Dans les versions affectées, le texte est
passé au Template() par défaut de Jinja2 et rendu sans sandbox :
template = Template(instructions)
instructions = template.render(ingredients=ingredients, scale=scale)
Parce que l'environnement Jinja2 par défaut expose le graphe d'objets Python, un
utilisateur authentifié capable de modifier une recette peut injecter une
expression de modèle qui remonte d'un tuple vide jusqu'à subprocess.Popen et
exécute une commande arbitraire sur le serveur. Le conteneur par défaut s'exécute
en tant que root, donc la commande s'exécute en tant que root.
Les instructions passent par une étape Markdown avant que Jinja2 ne les rende, et
cette étape supprime les underscores bruts. L'exploit encode donc les underscores
en hexadécimal (\x5f\x5f) et utilise le filtre attr() au lieu de la notation
par point. Voir payloads.txt.
compose-vulnerable.yml Tandoor 1.5.23 (vulnérable) plus PostgreSQL
compose-patched.yml Tandoor 1.5.24 (corrigé) plus PostgreSQL
payloads.txt Les deux payloads de preuve de concept et la sortie attendue
enumerate-popen.txt Aide pour trouver l'index de subprocess.Popen sur d'autres images
Les preuves dans ce dépôt ont été capturées sur la configuration ci-dessous. Toute version récente de Docker avec Compose v2 ou ultérieur reproduit le même résultat.
Démarrer la pile vulnérable :
docker compose -f compose-vulnerable.yml up -d
Attendre que les logs du conteneur web montrent un worker qui démarre, puis ouvrir
http://localhost:8080.
Enregistrer le premier compte. Le premier utilisateur devient le superutilisateur. Définir un nom d'espace lorsque demandé.
Créer une nouvelle recette. Dans le champ Steps > Instructions, coller le Payload 1 de
payloads.txt, puis enregistrer.
Ouvrir la recette pour la visualiser. Les instructions rendues affichent
<class 'subprocess.Popen'>, confirmant que l'injection atteint les internes de Python.
Répéter avec le Payload 2. La visualisation de la recette exécute maintenant whoami et affiche root.
Confirmer directement l'utilisateur du processus :
docker compose -f compose-vulnerable.yml exec web_recipes whoami
Arrêter la pile vulnérable :
docker compose -f compose-vulnerable.yml down
Démarrer la pile corrigée :
docker compose -f compose-patched.yml up -d
Répéter les mêmes étapes avec les mêmes payloads. Au lieu de s'exécuter, la vue de la recette
affiche maintenant Could not parse template code. Error: Undefined Error.
Pour tout réinitialiser et repartir d'une base de données vide, exécuter
docker compose -f compose-vulnerable.yml down -v. Le drapeau -v supprime les
volumes nommés, donc la prochaine exécution recrée la base de données à partir de zéro.
Le patch (commit e6087d5) remplace le Template() non sandboxé par le
SandboxedEnvironment de Jinja2 :
env = SandboxedEnvironment()
instructions = env.from_string(instructions).render(ingredients=ingredients, scale=scale)
Le sandbox surcharge la recherche d'attributs de Jinja2 pour interdire l'accès aux
attributs non sûrs tels que __class__, __base__, __subclasses__ et __getitem__.
Sans ceux-ci, le payload n'a aucun chemin d'un objet simple vers subprocess.Popen,
donc l'expression échoue au lieu de s'exécuter.
L'exploit atteint les attributs via le filtre attr() plutôt que la notation par point,
donc dans le code corrigé l'accès bloqué se manifeste comme une UndefinedError
plutôt qu'une SecurityError ; les deux sont gérées par la version sandboxée.
Ce dépôt reproduit une vulnérabilité publiquement divulguée et déjà corrigée à des fins éducatives. Ne l'exécuter que contre les conteneurs locaux fournis ici. Ne pas cibler des systèmes que vous ne possédez pas ou que vous n'êtes pas autorisé à tester.