跳转到帖子

文件上传是代码审计里很常见的入口,问题往往不在“是否允许上传”,而在上传后的处理链路:校验依据是否可靠、文件是否进入可解析目录、访问路径是否可控、后续异步处理是否信任了用户文件。本文整理一次后台上传功能审计中的经验,重点放在风险识别和加固方法,不涉及攻击利用细节。

背景

不少业务后台会提供头像、附件、合同、导入模板等上传能力。研发侧通常会做扩展名限制,例如只允许 jpg、png、pdf、xlsx。但从安全角度看,仅靠扩展名判断并不够,常见风险包括:

  • 只校验文件名后缀,未校验文件真实类型。
  • 上传目录位于 Web 可解析路径下,导致非预期解析。
  • 文件名由用户控制,存在覆盖、路径穿越或特殊字符问题。
  • 上传后由图片处理、文档转换、压缩解压等组件继续处理,引入二次风险。
  • 权限控制只放在前端页面,接口层缺少鉴权和对象级授权。

这类问题单点看不一定严重,但多个薄弱环节叠加后,往往会变成高风险入口。

审计场景

审计对象是一个常见的 Java Web 后台,上传接口大致逻辑如下:用户登录后选择文件,后端读取 MultipartFile,检查后缀是否在白名单内,然后把文件保存到配置的 uploadPath,最后返回可访问 URL。

简化后的伪代码如下:

String name = file.getOriginalFilename();
String ext = name.substring(name.lastIndexOf('.') + 1);
if (!allowExt.contains(ext.toLowerCase())) {
    throw new RuntimeException('invalid file type');
}
File dst = new File(uploadPath + '/' + name);
file.transferTo(dst);
return '/upload/' + name;

这段代码的问题不在于“没有写校验”,而是校验边界不完整:文件名、文件内容、存储位置、访问方式都没有被系统性约束。

技术分析

1. 文件名不能作为可信输入

getOriginalFilename 来自客户端请求,不能直接用于服务端存储。它可能包含特殊字符、重复文件名、过长文件名,甚至在不同平台下触发路径处理差异。安全做法是服务端生成随机文件名,并保留原始文件名作为展示字段存入数据库。

String safeName = UUID.randomUUID().toString().replace('-', '') + '.' + normalizedExt;
Path target = uploadRoot.resolve(safeName).normalize();
if (!target.startsWith(uploadRoot)) {
    throw new SecurityException('invalid path');
}

这里有两个点:一是不要信任原始文件名;二是路径拼接后必须 normalize 并检查是否仍在预期目录内。

2. 后缀校验只能作为第一层

后缀白名单有必要,但不能作为唯一依据。更稳妥的做法是组合校验:扩展名、MIME、文件头特征、实际解析结果。以图片上传为例,建议用服务端图片库尝试读取并重新编码,而不是简单保存原始文件。

BufferedImage image = ImageIO.read(file.getInputStream());
if (image == null) {
    throw new RuntimeException('not a valid image');
}
ImageIO.write(image, 'png', target.toFile());

重新编码可以去掉一部分非图片数据和异常结构,但也要注意图片处理库自身的安全更新,尤其是历史上 ImageMagick、ExifTool、部分文档转换组件都出现过高危漏洞。

3. 上传目录不要具备脚本解析能力

上传文件应当存放在 Web 根目录之外,业务访问时通过受控下载接口读取。如果确实需要静态访问,也应在 Web 服务器层明确禁止脚本解析,并限制可访问类型。

Nginx 示例配置可以参考:

location /upload/ {
    alias /data/app/upload/;
    autoindex off;
    default_type application/octet-stream;
    add_header X-Content-Type-Options nosniff;
}

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

这段配置的核心不是“拦某几个危险后缀”就完事,而是上传目录不应该进入任何动态解释链路。对于 Java 应用,也要确认容器不会把上传目录映射为可执行资源路径。

4. 下载接口也要做授权

很多系统只关注上传接口鉴权,忽略了下载 URL。如果上传文件包含合同、报表、证件等敏感数据,下载接口必须检查当前用户是否有权限访问该对象。不要把“知道 URL”当作访问权限。

FileMeta meta = fileService.getById(fileId);
if (!permissionService.canRead(currentUser, meta.getBizId())) {
    throw new AccessDeniedException('forbidden');
}
return streamFile(meta.getStorageKey());

建议文件表中保存 storageKey、业务对象 ID、上传人、文件类型、大小、哈希值、创建时间等字段,避免只靠 URL 管理文件。

关键审计步骤

实际审计时,我一般按下面顺序看,效率会比较高:

  • 定位所有上传入口:搜索 MultipartFile、CommonsMultipartResolver、transferTo、upload、import、attachment 等关键字。
  • 确认鉴权位置:接口是否需要登录,是否校验业务对象权限。
  • 检查文件名处理:是否使用原始文件名落盘,是否存在路径拼接、覆盖写入。
  • 检查类型校验:是否只有后缀判断,是否存在大小写、空白字符、双后缀等处理不一致问题。
  • 检查存储位置:是否位于 Web 根目录,是否可能被容器或网关解析。
  • 检查返回路径:是否直接返回可猜测 URL,是否有有效期或访问控制。
  • 检查后处理链路:图片压缩、文档预览、解压导入、OCR、杀毒扫描等组件是否安全配置。
  • 检查日志与告警:失败原因、异常文件、超大文件、频繁上传是否有记录。

常见问题与修复建议

问题风险建议
直接使用原始文件名覆盖、路径处理异常、信息泄露服务端生成随机存储名,原名仅用于展示
只校验后缀类型识别不可靠后缀、MIME、文件头、解析结果组合判断
上传目录可被解析扩大执行面放到 Web 根目录外,或显式禁用动态解析
下载 URL 无鉴权越权读取敏感文件通过文件 ID 下载,并校验业务权限
无限制大小和数量磁盘耗尽、服务阻塞限制单文件大小、总量、频率和用户配额
解压导入未校验目录穿越、压缩炸弹校验解压路径、文件数量、总大小和压缩比

服务端加固参考

一个相对稳妥的上传流程可以拆成几层:

  • 入口层:校验登录态、接口权限、文件大小、文件数量。
  • 类型层:按业务白名单校验扩展名和内容特征。
  • 存储层:生成随机 storageKey,保存到非 Web 目录或对象存储私有桶。
  • 处理层:必要时重新编码或转换,处理组件运行在低权限环境。
  • 访问层:通过下载接口鉴权,设置合理的 Content-Type 和 Content-Disposition。
  • 审计层:记录上传人、IP、文件哈希、业务对象、处理结果。

Linux 侧可以检查上传目录权限,避免应用用户之外的进程随意写入:

ls -ld /data/app/upload
find /data/app/upload -type f -perm /111 -ls
find /data/app/upload -type f -size +100M -ls

如果发现上传文件带可执行权限,一般应统一去掉:

find /data/app/upload -type f -exec chmod 0640 {} \;
find /data/app/upload -type d -exec chmod 0750 {} \;

注意点

  • 不要把黑名单当主要策略。黑名单容易漏,白名单加隔离更可靠。
  • 不要只在前端限制文件类型。前端限制只能提升体验,不能作为安全边界。
  • 不要把对象存储公开桶当默认方案。敏感文件建议私有桶加临时授权或后端代理下载。
  • 文档预览服务要单独关注。Office、PDF、图片解析链路经常引入第三方组件风险。
  • 日志中不要记录完整敏感文件内容,但应保留足够的审计字段。

总结

文件上传的安全核心是“不要让用户文件进入不受控的执行、解析和访问链路”。单独修一个后缀判断,通常只能解决表面问题。更长期有效的做法是:服务端命名、内容校验、存储隔离、下载鉴权、低权限处理、完整审计。

在代码审计中,如果看到上传接口直接保存原始文件名、上传目录可静态访问、返回 URL 无权限校验,基本就值得继续深挖。即使最终没有形成高危漏洞,这些点也应该作为工程加固项尽早处理。

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

0篇意见

推荐意见

没有意见。

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