跳转到帖子

文件上传是 Web 系统里很常见的能力,但也是代码审计和应急排查中反复出问题的模块。很多事故并不是因为某个单点校验缺失,而是上传入口、文件解析、存储位置、访问控制、异步处理几个环节叠加后,形成了可利用面。

这篇文章整理一套偏实战的审计思路,适合做代码审计、自查加固和上线前安全评审。内容只讨论防护和验证方法,不涉及未授权攻击操作。

一、背景:为什么上传模块容易出问题

一个完整的上传流程通常包含这些步骤:

  • 前端选择文件并提交 multipart/form-data 请求。
  • 后端接收文件流,读取文件名、后缀、Content-Type、大小等信息。
  • 后端进行格式校验、重命名、落盘或上传到对象存储。
  • 系统生成访问 URL,供用户预览、下载或进一步业务处理。
  • 后台任务可能对文件做解压、转码、缩略图、OCR、病毒扫描等处理。

风险也基本围绕这些环节产生:校验不严格、文件可执行、路径拼接不安全、对象存储权限过大、解析库存在历史漏洞、压缩包处理不当、下载接口越权等。

二、常见问题场景

审计中比较高频的问题包括:

  • 只依赖前端限制文件类型,后端没有强制校验。
  • 只检查文件后缀,不检查真实文件头和解析结果。
  • 使用用户原始文件名直接落盘,存在路径穿越、覆盖文件、特殊字符兼容问题。
  • 上传目录位于 Web 可访问目录,并且服务器对脚本后缀有执行能力。
  • 对象存储桶设置为公共读写,或上传后 URL 权限过宽。
  • 允许上传 HTML、SVG、XML 等主动内容,导致存储型脚本风险。
  • 解压 ZIP、RAR、7z 时未限制文件数量、总大小和解压路径。
  • 图片处理、文档转换组件版本过旧,存在已知解析漏洞。

三、审计入口怎么找

如果是白盒审计,建议先把上传入口摸清楚。不要只搜 upload,因为很多项目会用 attachment、avatar、import、media、file、oss、document 等命名。

rg -n "MultipartFile|Part|FormFile|multer|busboy|formidable|Request.Files|IFormFile" .
rg -n "upload|avatar|attachment|media|import|oss|cos|s3|bucket|fileName|originalFilename" .

Java/Spring 项目里重点看 MultipartFile 的使用方式;Node.js 项目里重点看 multer、busboy、formidable;PHP 项目看 $_FILES;.NET 项目看 IFormFile 或 Request.Files。

除了 Controller 层,还要继续追到 Service 层和存储层,因为很多项目在入口处只是临时接收,真正的风险发生在落盘、重命名、上传对象存储和生成访问 URL 的地方。

四、关键技术分析

1. 文件类型校验不要只看后缀

后缀名属于用户可控输入,只能作为第一层参考。更稳妥的方式是后缀白名单、MIME 白名单、魔数识别、业务解析结果一起判断。

例如头像上传,只允许 jpg、jpeg、png、webp,后端需要确认:

  • 扩展名归一化后在白名单内。
  • Content-Type 不能单独信任,只能作为辅助。
  • 读取文件头,确认符合对应格式。
  • 使用图片库实际解码一次,确认能被当作图片处理。
  • 重新编码输出,去掉潜在的附加内容和元数据。

伪代码示例:

String ext = getNormalizedExt(originalName);
if (!Set.of("jpg", "jpeg", "png", "webp").contains(ext)) {
    throw new IllegalArgumentException("unsupported file type");
}

byte[] head = readFirstBytes(file, 16);
if (!isAllowedMagic(head)) {
    throw new IllegalArgumentException("invalid file signature");
}

BufferedImage image = ImageIO.read(file.getInputStream());
if (image == null) {
    throw new IllegalArgumentException("invalid image content");
}

注意,魔数也不是万能的。部分格式允许复杂结构或嵌入内容,所以业务上能重编码的文件,建议统一重编码后再存储和分发。

2. 文件名必须重新生成

用户上传的原始文件名不应直接用于存储路径。常见问题包括:

  • 包含 ../ 或 ..\ 导致路径穿越。
  • 包含空字节、控制字符、Unicode 混淆字符,造成日志和文件系统表现不一致。
  • 同名覆盖已有文件。
  • 超长文件名导致异常或拒绝服务。

推荐做法是:原始文件名只用于展示,并经过转义后入库;实际存储名使用服务端生成的随机值或哈希值。

String storageName = UUID.randomUUID().toString().replace("-", "") + "." + ext;
Path base = Paths.get("/data/app/uploads").toRealPath();
Path target = base.resolve(storageName).normalize();

if (!target.startsWith(base)) {
    throw new SecurityException("invalid path");
}

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

如果文件落在 Web 根目录下,风险会明显升高。更稳妥的方式是上传目录放在应用目录之外,通过受控下载接口或独立静态域名访问。

Nginx 可以增加一层兜底,禁止上传目录内的脚本执行和危险类型解析:

location /uploads/ {
    alias /data/app/uploads/;
    autoindex off;
    add_header X-Content-Type-Options nosniff;

    location ~* \.(php|jsp|jspx|asp|aspx|sh|pl|py|cgi)$ {
        return 403;
    }
}

如果业务允许用户下载任意上传文件,建议统一使用 attachment 下载,而不是让浏览器直接渲染不可信内容。

Content-Disposition: attachment; filename="safe-name.bin"
X-Content-Type-Options: nosniff

4. SVG、HTML、XML 要谨慎开放

很多系统会把 SVG 当成图片处理,但 SVG 本质上是 XML,可能包含脚本、外链资源、事件属性等内容。如果业务不强依赖 SVG,建议直接禁止。

如果必须支持 SVG,需要做专门的安全净化,例如移除 script、foreignObject、事件属性、外部引用,并配合严格的 Content-Security-Policy。不要用普通图片白名单把 SVG 放进去。

5. 压缩包处理要限制解压行为

导入类功能经常允许上传 ZIP 或 Excel。ZIP 解压常见风险包括路径穿越、压缩炸弹、文件数量过多、单文件过大、嵌套压缩包递归展开。

审计解压逻辑时重点看:

  • 解压目标路径是否 normalize 并确认仍在基准目录下。
  • 是否限制压缩包总大小、解压后总大小、文件数量、目录层级。
  • 是否禁止符号链接和绝对路径。
  • 是否对每个解压出的文件再次做类型校验。
Path destDir = Paths.get("/data/app/imports/job-123").toRealPath();
Path out = destDir.resolve(entry.getName()).normalize();
if (!out.startsWith(destDir)) {
    throw new SecurityException("zip entry path traversal");
}

6. 对象存储不是天然安全边界

现在很多项目会把文件直接上传到 S3、OSS、COS、MinIO 等对象存储。这里容易出现两个误区:一是认为文件不在应用服务器就安全;二是为了方便把桶设置成公共读写。

建议检查:

  • 桶权限是否最小化,避免公共写。
  • 上传凭证是否短期有效,并限制路径前缀、文件大小和 Content-Type。
  • 用户是否只能访问自己有权限的对象。
  • 预签名 URL 有效期是否合理。
  • 回调接口是否校验签名,避免伪造上传成功通知。

如果使用前端直传,后端签发策略时不要只给一个宽泛的目录权限,应按用户、业务、时间窗口收敛。

五、建议的验证清单

检查项建议标准
后缀校验使用白名单,统一小写,处理多后缀和空格
内容校验校验魔数,并尽量使用业务解析库实际解析
文件名服务端生成存储名,原始文件名仅展示
存储路径normalize 后确认在基准目录内
执行权限上传目录禁止脚本执行,最好不放 Web 根目录
访问控制下载和预览接口必须校验资源归属
主动内容HTML、SVG、XML 默认禁止或净化后使用
压缩包限制数量、大小、层级,防路径穿越和压缩炸弹
对象存储最小权限、短期凭证、回调验签
审计日志记录用户、文件摘要、大小、类型、存储位置和处理结果

六、上线前可以做的安全回归

不需要复杂工具,也可以做一些基础回归。以下示例以本地测试环境为前提,用于验证后端是否真的做了限制。

# 上传一个超过限制大小的文件,确认被拒绝
curl -i -F "[email protected]" 

# 上传不在白名单内的后缀,确认被拒绝
curl -i -F "[email protected]" 

# 修改 Content-Type,确认后端不只依赖请求头
curl -i -F "[email protected];type=image/png"  A 上传的文件,用户 B 是否能通过猜测 ID 或对象 key 下载。这个问题在真实项目里比单纯文件类型绕过更常见。

七、几个容易忽略的注意点

  • 日志中不要直接打印未经处理的原始文件名,避免控制字符污染日志。
  • 文件哈希可以用于去重和溯源,但不要把哈希当作权限控制手段。
  • 图片 EXIF 可能包含 GPS、设备信息,面向公众展示前建议清理。
  • 导入 Excel 时也要限制行数、列数、公式和外部链接,避免资源消耗和数据外联风险。
  • 异步处理失败时,不要把临时文件长期留在可访问目录。
  • 杀毒扫描是加分项,但不能替代类型校验、权限控制和安全存储。
上传安全的核心不是某一个校验函数,而是把“不可信文件”从接收、处理、存储到访问的生命周期都管住。

八、总结

文件上传模块的安全设计建议遵循几条原则:后端强校验、白名单优先、服务端重命名、上传目录不可执行、访问接口做鉴权、对象存储最小权限、复杂文件谨慎解析。

实际审计时,不要只停留在 Controller 层的 if 判断。沿着文件流继续看落盘路径、对象 key、预览 URL、下载接口、异步任务和清理逻辑,通常更容易发现真正影响业务安全的问题。

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

0篇意见

推荐意见

没有意见。

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

SiteMap HACKHAT © 2026 All rights reserved.