Pillow FlashPix 堆溢出 RCE(CVE-2026-112398)
在安装了 olefile 的 Pillow 12.3.0 或更早版本中,解码特制 FlashPix 图像可让攻击者执行命令;Pillow 13.0.0 修复了 CVE-2026-112398。
要点速览
CVE-2026-112398 是 Pillow FlashPix JPEG 解码器中的严重堆缓冲区溢出漏洞(CVSS v4.0 9.2)。安装 olefile 的 12.3.0 及此前所有版本,在应用解码不可信图像时,都可能执行攻击者指定的命令。
- Pillow 的 FlashPix 插件解码 OLE2 容器(Microsoft 的复合文件二进制格式)中的 JPEG 图块。它根据 FlashPix 元数据声称的颜色模式确定解码缓冲区大小,却让 libjpeg 根据内嵌 JPEG 自身的分量数输出每行数据。如果文件声称每像素 2 字节,而内嵌 JPEG 每像素输出 4 字节,每一行都会溢出解码缓冲区。
- 任何打开并解码不可信上传文件的服务,如果运行 Pillow 12.3.0 或更早版本且安装可选 olefile 包,就会暴露:文件根据内容魔数而非扩展名识别,普通的
Image.open().load()调用即可触发溢出,无需用户交互。 - 厂商将其分类为 CWE-122(基于堆的缓冲区溢出)和 CWE-787(越界写入),公告包含能在原版 Pillow 12.2.0 上运行命令的概念验证。CISA 授权数据发布者(ADP)的补充信息将利用证据标为“poc”。截至 2026 年 10 月 24 日,漏洞未列入 KEV 目录,也没有已知在野攻击。
- 升级至 2026 年 10 月 16 日发布的 Pillow 13.0.0。如果无法升级,卸载 olefile 以禁用 FlashPix 和 Microsoft Image Composer 支持,或在 OLE2 文件到达 Pillow 前将其拒绝。
我是否受到影响?
以下条件全部成立时,您就受到影响:
- 应用或其导入的任何代码使用
Image.open()打开不可信文件,随后通过.load()、.thumbnail()、.tobytes()或其他读取像素数据的调用强制解码。 - 已安装的 Pillow 为 12.3.0 或更早版本。此前所有版本均受影响;CVE 记录将范围列为 13.0.0 之前。
- 环境中安装了可选 olefile 包。FlashPix 检测会导入它,Pillow 当前的打包配置将其声明为
fpx和mic功能的可选扩展依赖。
如果图像处理使用 Pillow 13.0.0 或更高版本,或未安装 olefile,就不受影响;后一种情况下,FlashPix 插件不会被加载。
PyPI 记录 pillow 过去一周有 141,847,434 次下载(2026 年 10 月 24 日核查)。Pillow 经常作为传递依赖进入环境,因此请审计每个环境,而不仅是直接声明依赖它的项目。
| 分支 | 受影响版本 | 修复版本 |
|---|---|---|
| 13.x | 无 | 13.0.0 及更高版本 |
| 12.x 及此前所有版本 | <= 12.3.0 | 13.0.0 |
检查环境中运行的版本,包括容器镜像和构建环境:
pip show pillow | grep -E "^(Name|Version)"
python -c "import PIL; print(PIL.__version__)"
python -c "import olefile; print('olefile', olefile.__version__)" 2>/dev/null || echo "olefile absent"
# pip requirements files
grep -i "^pillow" requirements*.txt
# Pipenv and pyproject.toml
grep -i "pillow" Pipfile.lock pyproject.toml 2>/dev/null
# Poetry
grep -A2 "name = \"pillow\"" poetry.lock
# Container images
docker run --rm <image> python -c "import PIL; print(PIL.__version__)"
如何修复?
升级至修复版本:
pip install --upgrade "pillow>=13.0.0"
Pillow 13.0.0 不再支持 Python 3.10,因此固定在 3.10 的环境需要升级 Python,或采用下述临时措施。
如果暂时无法升级,移除启用漏洞路径的依赖。FlashPix 支持需要 olefile,卸载它即可关闭漏洞路径:
pip uninstall -y olefile
这会移除 FlashPix 和 Microsoft Image Composer(MIC)支持,公告将两者列为依赖 olefile 的格式。局限很明确:正常处理 FlashPix 或 MIC 文件的服务将失去该能力,环境中其他导入 olefile 的代码也会失效。
网关也可以拒绝以 OLE2 魔数 D0CF11E0A1B11AE1 开头的上传文件。这与插件自身匹配的魔数相同,可以在入口阻止 FlashPix,而不受文件扩展名影响。
根本原因是什么?
FlashPix 插件根据 FlashPix 元数据确定 JPEG 解码缓冲区大小,而 libjpeg 根据内嵌 JPEG 头确定写入行的大小,两者之间没有任何一致性检查。
FlashPix 插件将容器中的颜色标识符映射为 Pillow 模式,位于 12.2.0 的 src/PIL/FpxImagePlugin.py 第 24 至 33 行:
MODES = {
# opacity
(0x00007FFE,): ("A", "L"),
# monochrome
(0x00010000,): ("L", "L"),
(0x00018000, 0x00017FFE): ("RGBA", "LA"),
...
(0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"),
}
攻击者通过向文件写入颜色标识符,选择使用哪一行映射。("RGBA", "LA") 这一行选择 rawmode LA,即每像素 2 字节。解码器根据该 rawmode 分配扫描行缓冲区,代码位于 12.2.0 的 src/decode.c 第 223 至 226 行:
state->bytes = (state->bits * state->xsize + 7) / 8; // "LA" gives 2 * xsize bytes per row
...
state->buffer = (UINT8 *)calloc(1, state->bytes);
随后,JPEG 解码器在 12.2.0 的 src/libImaging/JpegDecode.c 中将 rawmode 字符串映射为 libjpeg 输出颜色空间。CMYK、YCbCr 和 YCbCrK 等已识别 rawmode 会获得具体颜色空间。LA 和 YCC;P 未被识别,因此执行落入第 219 至 221 行的回退逻辑:
/* Disable decoder conversions. */
context->cinfo.jpeg_color_space = JCS_UNKNOWN;
context->cinfo.out_color_space = JCS_UNKNOWN;
在 JCS_UNKNOWN 下,libjpeg 不进行颜色转换,每像素输出 num_components 字节;该值由攻击者写入内嵌 JPEG 的 SOF 标记。如果将它设为 4,而缓冲区按 2 分配,那么第 252 行每次 jpeg_read_scanlines 调用都会向每像素 2 字节的缓冲区写入 4 分量数据行:
ok = jpeg_read_scanlines(&context->cinfo, &state->buffer, 1); // writes 4 * width into a 2 * width buffer
攻击者通过图像宽度控制溢出长度。溢出字节是解码后的像素,攻击者通过 JPEG 数据流塑造它们,而非直接写入。使此路径可达的魔数检查位于同一插件文件顶部:
def _accept(prefix: bytes) -> bool:
return prefix.startswith(olefile.MAGIC)
这是 OLE2 魔数,Pillow 对每个待打开文件都会按内容匹配。
如何利用?
攻击者构造一个 FlashPix 文件,其元数据声称每像素 2 字节,内嵌 JPEG 却声明 4 个分量。解码导致堆缓冲区溢出,公告中的 PoC 将其转为类型指针劫持,进而调用 system()。
公告的概念验证演示的攻击路径如下:
- 攻击者在自己的机器上使用公告提供的载荷构建器,生成独立 FlashPix 文件。构建不需要访问目标。
- 文件声明
("RGBA", "LA")模式,因此 Pillow 按每像素 2 字节确定图块缓冲区大小;同时内嵌的 JPEG 在 SOF 中声明 4 个分量,因此 libjpeg 每像素输出 4 字节。 - 受害服务通过任何上传或抓取路径收到该文件,并强制执行存在漏洞的解码。PoC 的完整操作只有
Image.open(path).load():
#!/usr/bin/env python3
# The entire vulnerable operation an application performs on an untrusted file.
import sys
from PIL import Image
Image.open(sys.argv[1]).load()
- 文件头中的 OLE2 魔数选择 FlashPix 插件,与文件名无关。首次
load()就会触发溢出。 - 溢出覆盖解码缓冲区旁的
bytes对象,在同一片可控堆内存中植入伪造类型对象和虚函数表。当 Pillow 释放输入缓冲区时,PyBuffer_Release沿被劫持的类型指针执行间接调用。PoC 将该调用指向system@plt,并把命令字符串放在第一个参数寄存器中,最终system("id")以运行图像处理的用户身份执行。
公开 PoC 使用固定的 Docker 镜像并禁用 ASLR,使硬编码地址保持有效;id 运行后,解释器因堆被破坏而报出 corrupted size vs. prev_size 并中止,但命令此时已经执行。对普通服务器,同样的溢出还需要处理地址随机化,因此概念验证证明的是漏洞和武器化路径,而不是一次成功的通用利用。
FlashPix 溢出如何触发?
整个过程是:OLE2 魔数选择 FlashPix 插件,LA rawmode 将行缓冲区定为每像素 2 字节,JCS_UNKNOWN 使 libjpeg 输出全部 4 个 JPEG 分量,超大的数据行随后覆盖下一个堆对象。
上传到服务"] --> B["Image.open() 匹配
OLE2 魔数 D0CF11E0"] B --> C["FlashPix 颜色 ID 选择
rawmode LA,每像素 2 字节"] C --> D["解码缓冲区分配
2 * width 字节"] D --> E["rawmode LA 未被识别,
libjpeg 设置为 JCS_UNKNOWN"] E --> F["内嵌 JPEG 声明 4 个
分量,按每像素
4 字节写入数据行"] F --> G["使用攻击者内容
进行堆越界写入"] G --> H["相邻对象的类型
指针被覆盖"] H --> I["释放 Hook 调用
攻击者目标,执行代码"]
攻击者能做什么?
解码特制文件会以运行图像处理的用户身份执行攻击者指定的命令,这意味着该进程及其持有的数据和权限被完全攻陷。公告将影响描述为在该进程上下文中执行任意命令。所需条件如下:
- 可通过网络访问一个端点,该端点打开受攻击者影响的文件并强制解码(
.load()、.thumbnail()、.tobytes()),且已安装可选 olefile 依赖。公告举例包括上传与缩略图服务、头像与媒体处理、文档与图像转换流水线。 - 无需用户交互,也不依赖扩展名伎俩:检测依据内容,因此名为
avatar.png但含 OLE2 字节的文件会进入 FlashPix 路径。 - PoC 的单次执行是在固定且关闭 ASLR 的镜像上演示的,因此启用地址随机化的真实目标需要额外利用工作,但底层越界写入在所有受影响版本中都会发生。
为什么修复有效?
修复在读取任何扫描行前,为 ImagingJpegDecode 添加边界检查,拒绝输出宽度或行大小超出解码缓冲区的数据流。补丁通过 #10131 合入,新增如下检查:
+ /* The scanline buffer was sized from the tile extents and the
+ unpacker, not from the JPEG header, so make sure the lines
+ libjpeg is about to produce actually fit in it. */
+ if (context->cinfo.output_width > (JDIMENSION)state->xsize ||
+ (size_t)context->cinfo.output_components * context->cinfo.output_width >
+ (size_t)state->bytes) {
+ jpeg_destroy_decompress(&context->cinfo);
+ state->errcode = IMAGING_CODEC_BROKEN;
+ return -1;
+ }
该检查拒绝宽度超出图块或分量超出缓冲区每像素容量的 JPEG,销毁解压器并报告“数据流损坏”,而不继续解码。它在缓冲区大小与 JPEG 头最终汇合的位置解决了这类不匹配:不是在仍映射 LA 模式的 FlashPix 表中,而是在解码器中。解码器现在拒绝写入无法容纳的数据行。
这是对超大扫描行 JPEG 输入的完整修复;它并未移除 JCS_UNKNOWN 路径本身,只要数据行能装入缓冲区,该路径就仍然正确。
如何检查是否已遭入侵?
该问题未公布入侵指标、域名或攻击者基础设施。溢出会留下三类可在自身环境中查找的证据:
- 图像处理工作进程的崩溃日志包含堆损坏消息,例如 PoC 留下的
corrupted size vs. prev_size中止,或打开上传图像的进程以 SIGSEGV 退出。 - 上传文件的前 8 字节是 OLE2 魔数
D0 CF 11 E0 A1 B1 1A E1,但文件名或声明类型不是 FlashPix,这正是插件自身魔数检查所描述的投递特征。 - 处理任务打开图像后执行意外命令或派生意外进程,因为执行发生在工作进程自身上下文中。
时间线
- 2026-10-06:Andrew Murray(radarhere)提交扫描行边界检查,通过 #10131 合入。
- 2026-10-16:Pillow 发布包含修复的 13.0.0。
- 2026-10-17:Pillow 发布公告 GHSA-5x8w-8jj5-q52h,评级为严重,CVSS v4.0 9.2,附完整概念验证,并向 Alex Bouffard(pebwalker)致谢。
- 2026-10-19:GitHub CNA 发布 CVE-2026-112398 的 CVE 记录。
- 2026-10-22:CISA 授权数据发布者(ADP)的补充信息将利用证据记录为“poc”。
- 2026-10-23:GitHub Advisory Database 发布并评审该公告。
- 2026-10-24:本文发布。
常见问题
服务必须安装 olefile 才会受影响吗?
是的。FlashPix 插件导入 olefile,Pillow 打包配置将其声明为 fpx 和 mic 扩展的可选依赖。没有它,插件不会加载,OLE2 文件会在解码前被拒绝。从未安装该包的环境不会进入漏洞路径。
重命名或阻止 .fpx 上传能阻止攻击吗?
不能。Image.open() 根据文件开头的 OLE2 魔数字节匹配 FlashPix 插件,因此扩展名与攻击和防御都无关。在上传边界按魔数前缀 D0CF11E0A1B11AE1 拒绝文件确实能阻止攻击,因为这就是插件要求的相同特征。
公开 PoC 是完整的远程利用吗?
它能够实际演示命令执行,但目标是固定的 Docker 镜像,且关闭 ASLR,从而固定载荷内的硬编码地址。在启用地址随机化的服务器上,同样的溢出需要额外安排或泄露地址。溢出本身及其赋予攻击者的堆控制能力,在任何受影响版本上都相同。
普通 JPEG 文件也危险吗?
普通 JPEG 不会进入这条路径。漏洞需要 FlashPix 容器选择 LA rawmode 和 JCS_UNKNOWN 回退,而普通 JPEG 打开流程不会这样做。补丁现在也会拒绝任何数据行超出缓冲区的 JPEG,因此这一通用问题类别已在解码器层面得到封堵。
Pillow 13.0.0 还修复了什么?
同一版本还修复了 CVE-2026-112900,即处理 EPS 输入时调用 Ghostscript 的参数注入问题,影响自 2.6.0 起的所有 Pillow 版本,项目将其评为严重,CVSS v4.0 9.2。升级到 13.0.0 可同时解决这两个问题。
审查您的图像处理服务
通过 Ostorlab 开始安全评估,然后使用上述依赖检查,验证每个图像处理环境中的 Pillow 版本及是否安装 olefile。将受影响部署升级至 Pillow 13.0.0 或更高版本。
参考资料
- Pillow 公告 GHSA-5x8w-8jj5-q52h,附概念验证和 Docker 实验环境,2026 年 10 月 17 日发布:https://github.com/python-pillow/Pillow/security/advisories/GHSA-5x8w-8jj5-q52h (2026 年 10 月 24 日核查)
- CVE-2026-112398 记录,包含 CVSS v4.0 9.2 向量和 CISA ADP“poc”补充信息:https://cveawg.mitre.org/api/cve/CVE-2026-112398 (2026 年 10 月 24 日核查)
- GitHub Advisory Database 条目:https://github.com/advisories/GHSA-5x8w-8jj5-q52h (2026 年 10 月 24 日核查)
- 修复提交 242a059a,“拒绝扫描行更宽或分量更多的 JPEG 数据流”,日期为 2026 年 10 月 6 日:https://github.com/python-pillow/Pillow/commit/242a059aedc1935097d9aa1dc3f099750467ba75 (2026 年 10 月 24 日核查)
- Pillow 13.0.0 版本,2026 年 10 月 16 日发布:https://github.com/python-pillow/Pillow/releases/tag/13.0.0 (2026 年 10 月 24 日核查)
- 12.2.0 中存在漏洞的 FpxImagePlugin 源码:https://raw.githubusercontent.com/python-pillow/Pillow/12.2.0/src/PIL/FpxImagePlugin.py (2026 年 10 月 24 日核查)
- 12.2.0 中存在漏洞的 JpegDecode.c 源码:https://raw.githubusercontent.com/python-pillow/Pillow/12.2.0/src/libImaging/JpegDecode.c (2026 年 10 月 24 日核查)
- Pillow 的 pyproject.toml,将 olefile 声明为 fpx 和 mic 可选扩展:https://raw.githubusercontent.com/python-pillow/Pillow/12.2.0/pyproject.toml (2026 年 10 月 24 日核查)
- pillow 的 PyPI 下载统计,过去一周 141,847,434 次下载:https://pypistats.org/packages/pillow (2026 年 10 月 24 日核查)
- 同一版本修复的 Ghostscript 参数注入关联公告:https://github.com/python-pillow/Pillow/security/advisories/GHSA-w889-685w-c8fh (2026 年 10 月 24 日核查)