
تحقق مرشح SSRF من نص اسم المضيف، لكن الوجهة الفعلية تم تحديدها لاحقًا بواسطة DNS. تلك الثغرة سمحت لعناوين URL الخاصة بـ Webhook التي يتحكم بها المهاجم بالوصول إلى أهداف loopback والبيانات الوصفية والشبكة الخاصة.
تحقق مرشح SSRF من نص اسم المضيف، ولكن الوجهة الفعلية تم تحديدها لاحقًا بواسطة DNS. سمحت هذه الفجوة لعناوين Webhook التي يتحكم فيها المهاجم بالوصول إلى loopback وبيانات التعريف وأهداف الشبكة الخاصة.
عثرت على هذه المشكلة أثناء مراجعة Typebot، منصة بناء روبوتات محادثة مفتوحة المصدر، مع سؤال أمني بسيط في ذهني:
ماذا يحدث إذا تحقق حماية SSRF من نص اسم المضيف، ولكن الوجهة الحقيقية تُقرر لاحقًا بواسطة DNS؟
في هذه الحالة، أدى هذا السؤال إلى خطأ حقيقي.
حماية SSRF الخاصة بـ Typebot لكتل Webhook / طلب HTTP تحققت فقط من:
لم تحل أسماء المضيف قبل السماح بالطلب.
هذا يعني أن اسم مضيف مثل ssrf-repro.example يمكن أن يبدو غير ضار أثناء التحقق، ثم يحل إلى:
127.0.0.1169.254.169.254وما زال يتم جلبها بواسطة عميل HTTP الخلفي.
أصبحت هذه المشكلة CVE-2026-34207.
Typebot: Typebot على GitHub
CVE: CVE-2026-34207
تم الإصلاح في: 3.16.0
أثر هذا على Typebot، منصة روبوتات محادثة مفتوحة المصدر واسعة الاستخدام. على موقعها الرسمي، يُقدم Typebot على أنه موثوق من أكثر من 650 شركة حول العالم. كما يعلن الموقع عن أكثر من 2 مليون محادثة شهريًا و أكثر من 1.5 مليون بوت منشور.
عنوان URL لـ Webhook يتحكم به المهاجم -> اسم المضيف يمر عبر التحقق الحرفي فقط SSRF -> لا يوجد حل DNS قبل قرار السماح -> عميل HTTP الخلفي يحل اسم المضيف إلى هدف داخلي -> يصل الطلب من جانب الخادم إلى loopback / بيانات التعريف / الشبكة الخاصة -> تصبح بيانات الرد متاحة من خلال سجلات التنفيذ
Typebot هو منصة بناء روبوتات محادثة.
يسمح للمستخدمين بإنشاء تدفقات يمكنها:
Webhook / طلب HTTPهذا يعني أن تنفيذ الطلبات الصادرة هو حدود أمنية حقيقية.
السؤال المهم هنا لم يكن ما إذا كان Typebot يدعم كتل Webhook.
السؤال الحقيقي كان:
هل يتحقق حماية SSRF من الوجهة الفعلية التي سيتصل بها الخادم، أم فقط نص اسم المضيف الذي يظهر في URL؟
في هذه الحالة، تحقق فقط من الشكل النصي أولاً.
كان هذا هو الخطأ.
تفشل دفاعات SSRF بطرق متوقعة جدًا.
في معظم الأوقات، الأخطاء المثيرة للاهتمام ليست:
169.254.169.254"localhost"الأخطاء الأقوى هي أخطاء الحدود:
كان هذا هو المكان المناسب للبحث هنا.
كان لدى Typebot بالفعل منطق تقوية SSRF لعناوين IP البيانات الوصفية الحرفية، و loopback، والنطاقات الخاصة، وخدع IP المشفرة.
هذا جعل السؤال التالي واضحًا:
ماذا لو لم يكن اسم المضيف خطيرًا حرفيًا، ولكنه يحل إلى وجهة خطيرة لاحقًا؟
هذا بالضبط ما حدث.
كانت المشكلة الجذرية هي التحقق من الوجهة بناءً على نص اسم المضيف بدلاً من عنوان IP الذي تم حله.
في التنفيذ الضعيف، validateHttpReqUrl():
http: و https:metadata.google.internal و metadata.goog و metadata و localhostهذا هو الجزء المهم.
إذا كان اسم المضيف قيمة عادية مثل:
ssrf-repro.example
فإن parseIPAddress(hostname) أعاد null، وتوقف المدقق عند هذا الحد.
لم يحدث حل DNS قبل الموافقة.
لذا المنطق الضعيف اختُزل عمليًا إلى:
const ip = parseIPAddress(hostname);
if (ip) {
validateIPAddress(ip);
}
هذا يعني:
النصف الثاني من الخطأ كان في مسار التنفيذ.
في executeHttpRequest()، قام Typebot أولاً بتشغيل التحقق ثم لاحقًا نفذ الطلب الفعلي باستخدام ky(request.url, ...).
لذا كان التسلسل:
هذا هو الثغرة بأكملها.
الفرق المهم هو مكان اتخاذ قرار الثقة.
الكثير من الأخطاء تبدو صغيرة إذا وصفتها بشكل سيء.
إذا وصفت هذا الخطأ على أنه:
"مرشح اسم المضيف كان غير مكتمل"
يبدو وكأنه مشكلة جودة.
هذه ليست المشكلة الحقيقية.
المشكلة الحقيقية كانت:
هذا ليس ضعفًا تجميليًا في التصفية.
هذا فشل في حدود الثقة.
ولأن منفذ HTTP سجل بيانات الرد في سجلات التنفيذ، لم تكن المشكلة عمياء حتى في أقوى الحالات.
لذا لم يكن هذا مجرد:
كان:
هذا هو ثغرة SSRF حقيقية.
استخدمت طبقتين للإثبات لأنهما أظهرتا شيئين مختلفين.
عزل إثبات المفهوم الأول السبب الجذري بشكل نظيف.
استخدمت أداة مساعدة محلية صغيرة:
127.0.0.1http://ssrf-repro.example:18080/...ssrf-repro.example حل إلى 127.0.0.1أظهر هذا الخطأ بالضبط:
أظهر المخرجات الملتقطة:
أثبت هذا فجوة التحقق مباشرة.
أظهر إثبات المفهوم الثاني الخطأ من خلال مسار الميزة الفعلي الذي يهم.
أبسط إعادة إنتاج كانت:
Webhook تشير إلى اسم المضيف غير الضار هذاكتلة تمثيلية تبدو هكذا:
{
"id": "blk-webhook",
"type": "Webhook",
"options": {
"webhook": {
"method": "GET",
"url": "http://ssrf-repro.example:8000/"
}
}
}
مع إدخال ملف المضيفين مثل:
127.0.0.1 ssrf-repro.example
ما زال المدقق يقبل URL لأن:
ssrf-repro.example لم يكن في blockedHostnameslocalhostparseIPAddress("ssrf-repro.example") أعاد nullثم قام عميل HTTP الخلفي بحل اسم المضيف إلى 127.0.0.1 واتصل على أي حال.
أثبت هذا الادعاء الكامل:
إثبات المفهوم الأول يثبت السبب الجذري.
إثبات المفهوم الثاني يثبت التأثير على المنتج.
هذا التقسيم مهم.
إذا أظهرت فقط:
"هذا المرشح يقبل اسم مضيف"
لم تظهر ما يكفي.
إذا أظهرت فقط:
"حدث طلب داخلي"
لم تعزل السبب.
التقرير الأقوى هو:
هذه هي القصة الكاملة.
تم تصنيف هذه المشكلة بشكل معقول كـ عالية.
تصنيف الاستشارة كان:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L
هذا منطقي.
لم يكن هذا SSRF غير مصادق عليه على مستوى الإنترنت بمفرده.
الامتيازات المطلوبة كانت منخفضة لأن الخطأ المستقل يتطلب فاعلًا يمكنه تكوين أو تشغيل كتلة Webhook / طلب HTTP.
لكن ضمن هذا الحد، كان التأثير خطيرًا:
هذه مشكلة SSRF قوية بذاتها.
وتصبح أكثر خطورة عند دمجها مع أخطاء أخرى توسع نطاق الوصول.