从安全角度理解 Pillow 的 open()verify()load()

图像上传并不是简单的“接收图片文件”,而是将外部输入交给解码器(解析器)处理的过程。 因此 Pillow 的三种方法,核心不在于功能描述,而在于何时调用(即何时打开攻击面)。


open() 并非“把像素加载到内存的函数”



Image.open()惰性操作。它只会打开文件并识别格式,像素数据可能尚未读取,文件句柄可能仍保持打开状态。

在安全/运维中,正确使用 open() 的方式很简单。

  • open() 先获取格式、宽高等轻量信息
  • 根据策略进行拦截:允许的格式、最大分辨率/像素数、上传容量限制
  • 再进入下一步(验证/解码)

也就是说,open() 应该作为“在解码前提取判断信息的工具”来使用。


verify() 能保证什么,不能保证什么

Pillow 的 verify() 试图检查文件是否损坏,但并不真正解码图像数据。若发现问题会抛出异常,且在 verify() 之后若继续使用图像,必须重新打开文件

从安全角度的结论有两点。

  • 优点:避免解码(即耗时操作),快速过滤“损坏文件”
  • 局限verify() 通过并不等同于“安全”,它仅表示“此时看起来没有明显损坏”。由于未完成完整解码,load() 时仍可能出现问题。

load() 在验证阶段随意调用会很危险



load() 实际上执行解码(包括解压缩)并将像素加载到内存。此时即成为 DoS(资源耗尽)攻击的入口。即使文件本身很小,解码后也可能膨胀到巨大的尺寸。

Pillow 会通过“解压缩炸弹(decompression bomb)”相关的警告/异常来处理,并设置默认阈值(如 128Mpx)等保护措施。

Django 也因同样原因,在图像上传验证中使用 verify() 而非 load()。源码中有注释说明:“load() 会把整张图像加载到内存,成为 DoS 向量”,实际做法是 Image.open() 后调用 verify()


Django/DRF 使用时:ImageField 再调用 verify() 可能是重复

Django 表单的 ImageField 验证内部已执行 Image.open() + verify()。DRF 的 serializers.ImageField 也同样委托给 Django 进行验证。

因此,如果你已经在 DRF 中使用 serializers.ImageField

  • validate() 中再次调用 verify() 以“检查是否损坏”通常是多余的
  • 若需要强制业务验证/额外安全检查,建议改用 FileField,自行设计验证管道,明确成本与责任。

如何确保用户上传文件的“安全”

上传文件安全处理流程图

最现实的答案是:

不要直接使用上传原始文件,而是让服务器解码后重新保存为新文件,再使用该结果。

  • open() 读取宽高/格式等低成本信息,先行按策略拦截
  • verify() 去除明显损坏的文件
  • 对通过的文件,在受限环境下解码后标准化为 RGB/RGBA等通用像素格式
  • 服务器按自己选择的格式重新编码生成新文件
  • 服务端只保存/提供服务器重编码后的文件

此策略的优点在于服务器控制最终输出的形态,可以大幅去除原始文件中不必要的元数据或异常结构。

但重编码最终仍包含 load() 的解码过程,因此需要先设置像素数/内存限制(防止解压缩炸弹),并尽量在工作进程/隔离进程中执行,以提升安全性。


总结

  • open()识别 + 低成本信息检查(像素可能尚未读取)
  • verify()一次性损坏过滤(无解码,后续使用需重新打开)
  • load()解码/内存使用起点(验证阶段禁止滥用)
  • 上传安全实务:仅信任服务器重编码后的结果(但需先设置限制/隔离)

相关阅读