资讯文章 资讯 · NEWS
用户上传的图片和附件:目录站那道不能省的服务端校验
2026-10-23 约 2,130 字 预计阅读 5 分钟 yiyunkj
用
开放收录的目录站,提交表单里基本都有一项「上传站点 Logo」或者「上传截图」。功能很小,出事的量级不小。
我见过最离谱的一次,是有人上传了一个 .jpg,后台照单全收存进了上传目录,然后那个目录恰好允许解析 PHP——文件名是 logo.jpg.php,前台的扩展名校验被双扩展名绕过去了。

图片来源:网络,版权归原作者所有,侵权请联系删除
前端校验不算校验
先把这件事说清楚:在 JS 里判断文件类型、限制大小,属于用户体验优化,不属于安全控制。攻击者用 curl 或者 Postman 直接打你的接口,一行前端代码都不会执行。
所有校验必须在服务端再来一遍。OWASP 的文件上传清单里第一条就是这个意思:客户端验证可以被绕过,服务端必须自己验。
服务端要验的五样东西
一、扩展名白名单,不是黑名单
定一个明确的允许列表,比如只要 jpg、png、webp。别去列举「禁止 php、jsp、asp」,你列举不完。同样功能的扩展名在不同服务器配置下有一堆变体:phtml、php5、pht、jspx,靠黑名单永远漏。
校验要在文件名解码之后做,而且要挡掉这几类绕过:
- 双扩展名:
logo.jpg.php - 空字节截断:
logo.php%00.jpg - 大小写变形:
logo.pHp - Windows NTFS 备用数据流:
logo.php::$DATA,简单起见只要文件名里出现冒号就拒
二、内容类型看魔数,不看 Content-Type
请求头里的 Content-Type 是客户端自己填的,想填什么填什么。真正可靠的是读文件开头几个字节:
| 类型 | 文件头(十六进制) |
|---|---|
| JPEG | FF D8 FF |
| PNG | 89 50 4E 47 |
| GIF | 47 49 46 38 |
| 25 50 44 46 | |
| ZIP(含 docx、xlsx) | 50 4B 03 04 |
读到的类型和声明的不一致,直接拒。这一条顺带挡掉了「把 PHP 文件前面拼一段 PNG 头」这种混血文件。
三、重新编码一遍图片
这一步很多人省了,其实很值。用图像处理库把上传的图重新解码再编码一次(顺便缩放、压缩),藏在 EXIF 里的脚本、注释段里的 payload 会被一起丢掉。这是「重编码」区别于「只校验」的地方——你不只是在判断它是不是一张图,你是在重新生成一张干净的图。
顺带解决另一件事:EXIF 里的 GPS 坐标。用户上传的截图常常带着拍摄位置,你把它原样存下来再公开出去,属于把别人的信息泄露出去。重编码默认就剥掉了。
四、文件名由服务端生成
永远不要用用户给的文件名当存储路径。生成一个 UUID 或者哈希,扩展名按检测出来的真实类型重新拼。原来的文件名只留在数据库里当显示用。
这一条同时挡掉了路径穿越(../../etc/passwd)、覆盖已有文件、以及用奇怪文件名打乱过滤规则的几种玩法。
五、大小限制要设在最外层
nginx 的 client_max_body_size 先卡一道,应用框架再卡一道,业务代码最后卡一道。只在应用代码里判断大小的问题是:nginx 可能已经把几百兆的请求体读进内存或临时文件了,应用层才说不要。限制要设在缓冲之前。
SVG 单独拎出来讲
SVG 是 XML,能带 <script>,能在浏览器里执行。用户传一个 SVG,你在前台用 <img> 标签显示它,脚本不会执行;但只要有任何一个地方把它当文档加载(直接访问那个 URL、放进 iframe、某些预览功能),就是一个存储型 XSS。
三种处理办法,按推荐程度排:
- 不接受 SVG。目录站的 Logo 场景,PNG 和 WebP 完全够用。
- 必须接受的话,过一遍 XML 清洗,去掉 script、事件属性、外部实体引用,同时关掉解析器的外部实体解析(XXE 也是从这进来的)。
- 存可以,但只在独立的、无 Cookie 的域名上提供,并且强制
Content-Disposition: attachment。
第 3 条的原理是:就算 SVG 里真有脚本被执行了,它在独立域上,读不到主站的会话 Cookie。这是隔离思路,不是清洗思路,两者可以叠加。
文件该存在哪儿
优先级从高到低:
- 独立的对象存储(S3、OSS、COS),私有读写,通过签名 URL 或者 CDN 提供
- 独立的无 Cookie 域名,物理上和主站分开
- webroot 之外,通过一个 handler 按 ID 映射(someid → file.ext),不要让用户能猜到真实路径
最后一条要强调:不要放在能解析脚本的目录里。上传目录如果配了 PHP 解析,前面所有的校验都是在给攻击者出附加题,他只要把可执行文件放进去就够了。上传目录应该禁止执行任何脚本,并且配 X-Content-Type-Options: nosniff,别让浏览器自己猜类型。
还有两件容易漏的
防 CSRF。上传接口必须带 CSRF token,否则攻击者可以在别的站点构造一个表单,让已登录的管理员在不知情的情况下上传文件。
限速和配额。单个用户每分钟能传几个、总共能占多大空间,要有上限。不然你遇到的不是攻击,是有人拿你的站当图床,磁盘三个月就满了。
落到目录站的具体配置
开放提交的表单里,Logo 和截图是必填还是选填?我倾向于选填,而且提交后进入人工审核队列再展示。审核队列本身也能挡掉大部分批量上传的垃圾。
另外,上传后的图如果要对外展示,记得给它配好宽高属性和 alt 文本。这既是安全之外的事,也是图片搜索那一路流量的入口——顺手就做了。