跳转到帖子

背景

文件上传是 Web 业务里很常见的功能,头像、附件、工单截图、富文本图片都会涉及。它看起来只是一个普通交互,但长期来看,上传点往往是安全边界最容易被忽略的位置:业务希望用户能自由提交文件,安全侧又必须确保这些文件不会变成脚本、钓鱼页面、恶意文档或横向移动入口。

这篇文章不讨论攻击利用细节,而是从代码审计和防护落地的角度,整理一次常见文件上传风险的分析思路。适合做安全自查、研发改造和上线前 review 时参考。

典型场景

很多系统的上传逻辑大致如下:

  • 前端限制文件后缀,例如只允许 jpg、png、pdf。
  • 后端接收 multipart 请求,把文件保存到某个静态目录。
  • 数据库记录原始文件名和访问 URL。
  • 用户后续可以通过 URL 直接访问该文件。

问题通常出在“前端校验”和“后端保存”之间。前端限制只能提高体验,不能作为安全边界;后端如果只判断文件名后缀,也容易忽略文件内容、存储位置、访问方式、权限隔离等问题。

问题分析

做文件上传审计时,我一般不会只盯着一个 if 判断,而是沿着文件生命周期看完整链路:

  • 接收阶段:是否限制大小、类型、数量、频率。
  • 校验阶段:是否只依赖后缀、Content-Type、前端参数。
  • 命名阶段:是否保留用户原始文件名,是否存在路径拼接风险。
  • 存储阶段:是否落在 Web 可执行目录,是否和业务代码同权限。
  • 访问阶段:是否可以直接访问,是否需要鉴权,响应头是否合理。
  • 处理阶段:是否调用图片压缩、文档预览、解压等第三方组件。

一个上传点是否危险,往往不取决于单个判断,而取决于多个小问题叠加。例如后端只校验后缀、文件保存到静态目录、服务端对该目录启用了脚本解析,这种组合就会把普通上传功能变成高风险入口。

常见风险点

1. 仅依赖后缀或 Content-Type

后缀和 Content-Type 都可以由客户端提交,不能作为唯一依据。比较稳妥的方式是多层校验:扩展名白名单、MIME 探测、文件魔数检查、必要时进行解码重写。

以图片上传为例,不能只判断 .jpg 或 image/jpeg,还应确认文件头和真实格式,并在服务端重新处理生成新图片。

允许类型示例:
- image/jpeg
- image/png
- image/gif

不建议:
- 根据用户传入的 filename 判断是否安全
- 根据请求头 Content-Type 直接放行
- 使用黑名单排除少数危险后缀

2. 用户文件名直接落盘

保留原始文件名方便展示,但不应该直接用于服务器存储路径。原始文件名可能包含特殊字符、重复名称、超长字符串,也可能影响日志、下载响应或路径拼接逻辑。

实践中建议将“展示名”和“存储名”分离:

  • 数据库保存原始文件名,仅用于页面展示。
  • 服务端生成随机文件名或基于哈希的文件名。
  • 扩展名由服务端根据识别结果重新确定。
  • 路径拼接统一使用安全 API,不手写字符串拼接。
存储路径示例:
/uploads/2026/10/10/9f2b1b9a8c0f4e2d.png

数据库字段示例:
original_name: 报销截图.png
storage_name: 9f2b1b9a8c0f4e2d.png
mime_type: image/png
size: 184231
owner_id: 10086

3. 上传目录具备脚本执行能力

上传目录应当只作为静态资源目录,不应具备任何脚本执行能力。这个点在老系统、混合部署系统里尤其常见,例如业务代码、附件目录、临时目录都在同一个 Web 根目录下。

Nginx 场景下可以明确禁止上传目录进入脚本处理逻辑,并补充响应头:

location /uploads/ {
    alias /data/app/uploads/;
    autoindex off;
    types { }
    default_type application/octet-stream;
    add_header X-Content-Type-Options nosniff;
    add_header Content-Disposition "attachment";
}

location ~ \.php$ {
    fastcgi_pass unix:/run/php/php-fpm.sock;
    include fastcgi_params;
}

需要注意 location 匹配顺序,避免上传目录里的文件被后面的脚本规则接管。Apache、Tomcat、IIS 也应按各自机制关闭上传目录解析能力。

4. 文件可直接公开访问

并不是所有上传文件都应该公开访问。工单附件、合同、截图、日志包等通常包含敏感信息,如果保存后直接生成公网 URL,会产生越权访问和信息泄露风险。

更稳妥的方式是通过业务接口进行下载鉴权,而不是暴露真实存储路径:

  • 下载接口校验登录态和资源归属关系。
  • 内部存储路径不直接对外暴露。
  • 短期访问可以使用带过期时间的签名 URL。
  • 敏感附件默认强制下载,避免浏览器直接渲染。

5. 文件处理组件带来的二次风险

上传本身只是第一步,后续的图片压缩、文档转预览、压缩包解压、OCR、杀毒扫描等处理流程,也可能引入风险。安全审计时要关注这些组件的版本、运行权限和输入限制。

常见建议:

  • 图片处理库保持更新,避免使用多年未维护版本。
  • 文档转换服务与主业务隔离部署,使用低权限账号运行。
  • 压缩包处理限制总大小、文件数量、目录层级。
  • 处理任务设置超时,避免被大文件或畸形文件拖垮。
  • 临时目录定期清理,避免敏感文件长期残留。

关键防护步骤

下面是一套比较通用的上传防护清单,可以按业务实际裁剪。

第一步:使用白名单策略

只允许业务确实需要的类型,不做“允许所有,排除危险类型”的黑名单设计。

推荐思路:
1. 扩展名白名单:jpg、jpeg、png、pdf
2. MIME 白名单:image/jpeg、image/png、application/pdf
3. 文件魔数校验:确认真实格式
4. 服务端统一重命名
5. 超出类型直接拒绝

第二步:限制文件大小和上传频率

大小限制不仅是安全要求,也是稳定性要求。上传入口如果没有限制,很容易造成磁盘占满、内存抖动或队列积压。

Nginx 示例:
client_max_body_size 10m;
client_body_timeout 15s;

业务侧建议:
- 单文件大小限制
- 单用户每日上传数量限制
- 单业务对象附件数量限制
- 上传失败次数监控

第三步:隔离存储与执行环境

上传文件最好存放在业务代码目录之外。如果条件允许,可以使用对象存储或独立文件服务。核心原则是:上传内容不应获得代码执行环境的能力。

  • 上传目录与应用代码目录分离。
  • 上传目录只读访问,写入由专用服务完成。
  • 处理服务使用低权限账号。
  • 生产环境禁止目录浏览。
  • 定期检查上传目录权限和异常文件类型。

第四步:下载访问走鉴权接口

如果文件有权限属性,不建议直接把物理路径或永久 URL 暴露给前端。下载接口至少要校验用户身份、资源归属、文件状态。

下载鉴权逻辑示例:
1. 用户登录校验
2. 根据 file_id 查询文件记录
3. 判断 owner_id、项目成员或业务权限
4. 校验文件状态是否可访问
5. 设置安全响应头后输出文件流

第五步:补充日志和监控

上传点很适合做安全监控,因为异常行为相对明显。建议记录必要字段,但不要把文件内容写入日志。

字段说明
user_id上传用户
ip来源地址
original_name原始文件名
mime_type识别后的类型
file_size文件大小
result成功、拒绝、扫描失败等
reason拒绝原因,如类型不符、超限

可以重点关注这些异常:

  • 同一用户短时间大量上传失败。
  • 频繁尝试非常规扩展名。
  • 文件头和扩展名长期不一致。
  • 上传后立刻大量访问。
  • 上传目录出现业务不允许的文件类型。

代码审计时的关注点

如果是做代码审计,可以从路由入口开始,顺着调用链看以下位置:

  • 上传接口是否有登录校验。
  • 权限是否和业务对象绑定,例如项目、工单、订单。
  • 文件类型判断是否在后端完成。
  • 是否存在原始文件名拼接路径。
  • 是否将上传目录放在 Web 根目录。
  • 是否对图片、PDF、压缩包做了额外处理。
  • 处理失败时临时文件是否清理。
  • 返回给前端的 URL 是否可被枚举。

很多问题不是单独看上传函数就能发现的,尤其是越权下载、公开访问和文件处理链路,必须结合业务权限模型一起看。

上线前检查清单

  • 是否有明确的文件类型白名单。
  • 是否校验真实文件格式,而不是只看后缀。
  • 是否限制大小、数量、频率。
  • 是否使用服务端生成的安全文件名。
  • 上传目录是否禁止脚本执行。
  • 敏感文件访问是否经过鉴权。
  • 是否设置 X-Content-Type-Options: nosniff。
  • 是否对可预览文件做了安全响应头控制。
  • 第三方处理组件是否隔离和更新。
  • 是否具备日志、告警和清理机制。

注意点

不要把“前端限制文件类型”当成安全措施,它只能算交互优化。

另外,有些系统为了兼容历史数据,会保留旧上传目录或旧访问路径。改造时不要只修新接口,也要检查历史文件目录、旧域名、反向代理规则和 CDN 缓存策略,否则风险仍然存在。

如果业务必须支持复杂文件,例如 Office、PDF、压缩包,建议把预览、转换、扫描服务和主业务拆开。即使处理组件出现问题,也尽量限制影响范围。

总结

文件上传安全不是一个简单的“判断后缀”问题,而是一套围绕输入、存储、访问和处理的边界设计。比较稳的做法是:白名单校验、服务端重命名、存储执行隔离、下载鉴权、处理服务低权限隔离,再配合日志监控和定期巡检。

从经验看,上传点最容易在业务快速迭代时被放松要求。建议把上传能力抽象成统一组件,由平台层集中处理校验、存储和访问控制,避免每个业务线各写一套逻辑。这样不仅减少漏洞,也方便后续审计和应急排查。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…