
إثبات مفهوم لثغرة CVE-2025-23211؛ يعيد إنتاج الثغرة المرتبطة بخصوصية البيانات لدعم التحقق الأمني المصرح به، واختبار التصحيحات، والتحقق من الإجراءات التخفيفية.
بيئة مكتفية ذاتيًا وقابلة لإعادة الإنتاج توضح CVE-2025-23211، وهي ثغرة حقن القوالب من جانب الخادم (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 هو تطبيق مفتوح المصدر لاستضافة الذات لتخطيط الوجبات وإدارة الوصفات. الواجهة الخلفية هي 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، يمكن لمستخدم مُصادَق عليه قادر على تحرير وصفة أن يحقن تعبير قالب ينتقل من tuple فارغ إلى subprocess.Popen وينفّذ أمرًا عشوائيًا على الخادم. تعمل الحاوية الافتراضية بصلاحيات root، لذا يُنفَّذ الأمر بصلاحيات root.
تمر التعليمات عبر خطوة Markdown قبل أن تعرضها Jinja2، وتلك الخطوة تُزيل الشرطات السفلية الخام. لذلك يقوم الاستغلال بترميز الشرطات السفلية بالنظام الست عشري (\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
كرّر الخطوات نفسها مع الحمولات نفسها. بدلًا من التنفيذ، يعرض عرض الوصفة الآن
Could not parse template code. Error: Undefined Error.
لإعادة تعيين كل شيء والبدء من قاعدة بيانات فارغة، شغّل
docker compose -f compose-vulnerable.yml down -v. يزيل العلم -v الأحجام
المسمّاة، لذا يُعيد التشغيل التالي إنشاء قاعدة البيانات من الصفر.
يستبدل التصحيح (commit e6087d5) استخدام Template() غير المحمي بصندوق رمل
SandboxedEnvironment من Jinja2:
env = SandboxedEnvironment()
instructions = env.from_string(instructions).render(ingredients=ingredients, scale=scale)
يتجاوز صندوق الرمل البحث عن السمات في Jinja2 لمنع الوصول إلى السمات غير الآمنة
مثل __class__ و__base__ و__subclasses__ و__getitem__. بدونها، لا يجد
الاستغلال مسارًا من كائن عادي إلى subprocess.Popen، لذا يفشل التعبير بدلًا من
التنفيذ.
يصل الاستغلال إلى السمات عبر مرشّح attr() بدلًا من تدوين النقطة، لذا في
الكود المُرقّع يظهر الوصول المحظور كـ UndefinedError بدلًا من SecurityError؛
كلاهما يُعالَج بواسطة الإصدار المحمي بصندوق الرمل.
يعيد هذا المستودع إنتاج ثغرة مُفصَح عنها علنًا ومُرقّعة بالفعل لأغراض تعليمية. شغّله فقط ضد الحاويات المحلية المقدَّمة هنا. لا تستهدف أنظمة لا تملكها أو غير مصرّح لك باختبارها.