bigywater.tech
Flask · 126

文件上传类型校验

上传的文件怎么确认它真是图片?扩展名和 Content-Type 能信吗?校验到什么程度才算够?这篇只验证防御端:拿一份朴素实现和一份校验内容的实现,用各种「像又不像」的文件逐个对照,还记录了我自己代码里一个会误拒中文文件名的缺陷。

400
纯文本冒充 PNG 在内容校验下的结果(朴素实现返回 200)
uuid
保存到磁盘的文件名,不使用用户提供的名字
413
请求体超过 MAX_CONTENT_LENGTH 时的状态码

只看扩展名与声明的类型

实测 · 本机执行输出
  真正的 PNG,x.png,image/png                      → 200 {"accepted": true, "client_mimetype": "image/png", "name": "x.png"}
  纯文本,x.png,声明 image/png                       → 200 {"accepted": true, "client_mimetype": "image/png", "name": "x.png"}
  纯文本,X.PNG,声明 image/png                       → 200 {"accepted": true, "client_mimetype": "image/png", "name": "X.PNG"}
  真正的 PNG,x.txt,image/png                      → 400 {"accepted": false}

上一篇:路径穿越与文件下载;下一篇:Host Header 校验。

校验内容(含一个中文文件名的缺陷)

实测 · 本机执行输出
  真正的 PNG                                      → 200 {"original": "photo.png", "saved_as": "49fda2d0cad242aca0c7a35d04e53666.png", "size": 74}
  真正的 JPEG,扩展名 jpeg                            → 200 {"original": "p.jpeg", "saved_as": "64811b8bf522417790e234373204c7ef.jpg", "size": 633}
  纯文本冒充 PNG                                    → 400 {"error": "文件内容不是允许的图片类型"}
  PNG 内容,扩展名写成 jpg                             → 400 {"content": "png", "error": "扩展名与内容不一致", "ext": "jpg"}
  PNG 签名 + 后面是垃圾                               → 400 {"detail": "OSError", "error": "图片无法解析"}
  扩展名不在白名单                                     → 400 {"error": "扩展名不允许", "ext": "svg"}
  没有扩展名                                        → 400 {"error": "扩展名不允许", "ext": ""}
  文件名含路径与空格                                    → 200 {"original": "my_pic.png", "saved_as": "ba3ff713f92743be81e16d723dce9063.png", "size": 74}
  中文文件名                                        → 400 {"error": "扩展名不允许", "ext": ""}
  磁盘上保存的文件: ['49fda2d0….png', '64811b8b….jpg', 'ba3ff713….png'] (名字是随机的 uuid,不是用户给的)
  没有 f 字段                                      → 400 {"error": "没有文件"}

大小限制

实测 · 本机执行输出
  上传约 100 KB → 200
  上传约 199 KB → 200
  上传约 201 KB → 413
  上传约 1000 KB → 413

签名与解析

实测 · 本机执行输出
  被截断的 PNG(签名与头部完整,数据缺失)                       → 400 {"detail": "UnidentifiedImageError", "error": "图片无法解析"}

本篇是防御向的验证:没有构造任何恶意文件,「冒充」用的是一段普通文本。

分步演示

文件上传类型校验
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。Pillow 12.3.0
    • 没有验证:SVG、PDF、Office 文档等格式的校验、压缩包与图片炸弹(超大像素)的防护、病毒扫描、把上传目录放在 Web 根之外与对象存储的做法、图片重新编码后的元数据清除、流式分块上传的大小限制
    京ICP备2026067502号-1京公网安备11010502063487号