
Proof-of-concept für CVE-2025-23211; reproduziert die datenschutzrelevante Schwachstelle, um autorisierte Sicherheitsvalidierung, Patch-Tests und die Verifizierung von Gegenmaßnahmen zu unterstützen.
Eine in sich geschlossene, reproduzierbare Umgebung, die CVE-2025-23211 demonstriert, eine Server-Side Template Injection (SSTI)-Schwachstelle in Tandoor Recipes, die zu Remote Code Execution führt. Das Repository startet die verwundbare Version, zeigt den Exploit, startet dann die gepatchte Version und zeigt, dass dieselbe Eingabe abgelehnt wird.
| 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 ist eine quelloffene, selbst gehostete Anwendung für Essensplanung und Rezeptverwaltung. Das Backend ist Django (Python), bereitgestellt durch Gunicorn, das Frontend ist eine Vue.js-Single-Page-Anwendung, und die Daten werden in PostgreSQL gespeichert.
Tandoor ermöglicht es Benutzern, Jinja2-Template-Ausdrücke in den Anweisungstext eines Rezepts zu schreiben, eine Funktion, die für Aufgaben wie die Skalierung von Zutatenmengen gedacht ist. Diese Anweisungen werden auf dem Server gerendert.
Der Anweisungstext wird von render_instructions() in cookbook/helper/template_helper.py verarbeitet. In betroffenen Versionen wird der Text an Jinja2s Standard-Template() übergeben und ohne Sandbox gerendert:
template = Template(instructions)
instructions = template.render(ingredients=ingredients, scale=scale)
Da die Standard-Jinja2-Umgebung den Python-Objektgraphen offenlegt, kann ein authentifizierter Benutzer, der ein Rezept bearbeiten kann, einen Template-Ausdruck einschleusen, der von einem leeren Tupel zu subprocess.Popen wandert und einen beliebigen Befehl auf dem Server ausführt. Der Standard-Container läuft als root, sodass der Befehl als root ausgeführt wird.
Die Anweisungen durchlaufen einen Markdown-Schritt, bevor Jinja2 sie rendert, und dieser Schritt entfernt rohe Unterstriche. Der Exploit hex-kodiert daher Unterstriche (\x5f\x5f) und verwendet den attr()-Filter anstelle der Punktnotation. Siehe 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
Die Belege in diesem Repository wurden mit dem unten beschriebenen Setup erfasst. Jede aktuelle Docker-Version mit Compose v2 oder neuer reproduziert dasselbe Ergebnis.
Starten Sie den verwundbaren Stack:
docker compose -f compose-vulnerable.yml up -d
Warten Sie, bis die Logs des Web-Containers den Start eines Workers anzeigen, und öffnen Sie dann http://localhost:8080.
Registrieren Sie das erste Konto. Der erste Benutzer wird zum Superuser. Legen Sie einen Space-Namen fest, wenn Sie dazu aufgefordert werden.
Erstellen Sie ein neues Rezept. Fügen Sie im Feld Steps > Instructions Payload 1 aus payloads.txt ein und speichern Sie.
Öffnen Sie das Rezept, um es anzuzeigen. Die gerenderten Anweisungen zeigen <class 'subprocess.Popen'>, was bestätigt, dass die Injection die Python-Interna erreicht.
Wiederholen Sie den Vorgang mit Payload 2. Beim Anzeigen des Rezepts wird nun whoami ausgeführt und root angezeigt.
Bestätigen Sie den Prozessbenutzer direkt:
docker compose -f compose-vulnerable.yml exec web_recipes whoami
Stoppen Sie den verwundbaren Stack:
docker compose -f compose-vulnerable.yml down
Starten Sie den gepatchten Stack:
docker compose -f compose-patched.yml up -d
Wiederholen Sie dieselben Schritte mit denselben Payloads. Anstatt ausgeführt zu werden, zeigt die Rezeptansicht nun Could not parse template code. Error: Undefined Error.
Um alles zurückzusetzen und mit einer leeren Datenbank zu beginnen, führen Sie
docker compose -f compose-vulnerable.yml down -v aus. Das Flag -v entfernt die
benannten Volumes, sodass der nächste Lauf die Datenbank von Grund auf neu erstellt.
Der Patch (Commit e6087d5) ersetzt das nicht gesandboxte Template() durch Jinja2s SandboxedEnvironment:
env = SandboxedEnvironment()
instructions = env.from_string(instructions).render(ingredients=ingredients, scale=scale)
Die Sandbox überschreibt Jinja2s Attribut-Lookup, um den Zugriff auf unsichere Attribute wie __class__, __base__, __subclasses__ und __getitem__ zu verbieten. Ohne diese hat der Payload keinen Pfad von einem einfachen Objekt zu subprocess.Popen, sodass der Ausdruck fehlschlägt, anstatt ausgeführt zu werden.
Der Exploit erreicht Attribute über den attr()-Filter statt über die Punktnotation, sodass im gepatchten Code der blockierte Zugriff als UndefinedError statt als SecurityError auftritt; beide werden von der gesandboxten Version behandelt.
Dieses Repository reproduziert eine öffentlich offengelegte und bereits gepatchte Schwachstelle zu Bildungszwecken. Führen Sie es nur gegen die hier bereitgestellten lokalen Container aus. Richten Sie es nicht auf Systeme, die Ihnen nicht gehören oder die Sie nicht testen dürfen.