跳转到帖子

背景

文件上传功能在业务系统里很常见,头像、附件、工单截图、合同材料都可能涉及上传。也正因为入口普遍、实现差异大,它一直是 Web 安全审计里的高频风险点。

这篇文章整理的是一次偏防守视角的复盘:从代码审计中发现上传接口存在缺陷,到复现风险、分析成因,再到给出可落地的修复方案。文中不会涉及真实目标攻击,也不提供恶意利用引导,重点放在审计方法和加固思路。

问题场景

某业务系统提供了一个附件上传接口,前端限制只能上传图片或 PDF。开发同学最初的判断是:前端已经限制了文件类型,后端也检查了扩展名,因此风险不大。

但在代码审计中可以看到几个典型问题:

  • 文件类型主要依赖原始文件名后缀判断。
  • 上传目录位于 Web 可访问路径下。
  • 保存文件名使用用户提交的文件名拼接。
  • 未对文件内容做可信校验。
  • 缺少统一的大小限制、权限控制和日志记录。

单个问题未必一定导致严重后果,但几个点叠加后,上传功能就可能从普通业务入口变成高风险入口。

代码片段与风险点

审计时看到的代码大致类似下面这种写法。这里做了脱敏和简化,只保留关键逻辑。

String fileName = multipartFile.getOriginalFilename();
String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);

if (!Arrays.asList("jpg", "png", "pdf").contains(suffix.toLowerCase())) {
    throw new RuntimeException("file type not allowed");
}

String uploadDir = request.getServletContext().getRealPath("/upload/");
File dest = new File(uploadDir + fileName);
multipartFile.transferTo(dest);

return "/upload/" + fileName;

这类实现的问题比较集中:

  • 信任原始文件名:原始文件名来自客户端,不能直接作为服务端保存路径的一部分。
  • 后缀判断不足:后缀只能作为辅助信号,不能代表真实内容类型。
  • Web 目录直出:上传目录如果能被 Web 容器直接访问,风险面会明显扩大。
  • 路径拼接不安全:直接拼接路径容易引入路径穿越、覆盖文件等问题。
  • 缺少二次处理:图片类文件没有重新编码,元数据和异常结构没有被清理。

技术分析

文件上传审计通常不要只看“有没有限制后缀”,而是要从数据流角度看完整链路:

  • 请求入口:接口是否需要认证,是否有权限校验。
  • 文件解析:是否使用可信库解析内容,异常是否可控。
  • 文件校验:是否校验大小、扩展名、MIME、魔数、内容结构。
  • 文件存储:是否使用随机文件名,是否存到非 Web 目录。
  • 文件访问:是否通过受控下载接口返回,而不是静态目录直出。
  • 后续处理:是否进入图片处理、PDF 解析、OCR、压缩解压等链路。

实际项目里,风险往往不止在上传当下。比如上传后的文件会被异步任务处理,或被其他服务拉取解析,这时还要关注解析组件版本、沙箱隔离、临时目录权限等问题。

复现验证思路

为了避免测试动作影响业务环境,建议在本地或测试环境复现。验证目标不是“打穿系统”,而是确认后端实际安全边界是否符合预期。

可以用以下几个方向做安全验证:

  • 上传超大文件,确认大小限制是否在服务端生效。
  • 上传后缀大小写混合的文件,确认判断是否一致。
  • 上传文件名包含特殊字符的样本,确认服务端是否安全重命名。
  • 上传内容与后缀不匹配的样本,确认是否做内容校验。
  • 上传同名文件,确认是否会覆盖已有文件。
  • 未登录或低权限账号访问上传接口,确认鉴权是否完整。

例如在测试环境中,可以用普通文本内容构造一个伪装成图片后缀的样本,验证后端是否仅依赖后缀:

printf 'this is not a real image' > sample.jpg
curl -i -X POST \
  -H "Cookie: SESSION=your_test_session" \
  -F "[email protected];type=image/jpeg" \
   使用白名单并统一规范化

后缀可以保留,但只能作为第一层过滤。需要先对文件名做规范化处理,并且不使用用户原始文件名作为落盘文件名。

private static final Set<String> ALLOWED_EXT = Set.of("jpg", "jpeg", "png", "pdf");

private String getSafeExt(String originalName) {
    if (originalName == null || !originalName.contains(".")) {
        throw new IllegalArgumentException("invalid file name");
    }
    String ext = originalName.substring(originalName.lastIndexOf('.') + 1).toLowerCase(Locale.ROOT);
    if (!ALLOWED_EXT.contains(ext)) {
        throw new IllegalArgumentException("file type not allowed");
    }
    return ext;
}

2. 校验真实文件类型

对于图片,可以读取文件头并尝试使用图片库解码。对于 PDF,可以校验基础结构并限制解析行为。MIME 头来自客户端,不应直接信任。

private boolean isPng(byte[] header) {
    byte[] png = new byte[] {(byte)0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
    if (header.length < png.length) return false;
    for (int i = 0; i < png.length; i++) {
        if (header[i] != png[i]) return false;
    }
    return true;
}

如果业务只允许图片,建议上传后重新编码生成新图片,而不是原样保存用户文件。重新编码能清理一部分异常结构和元数据。

3. 存储到非 Web 目录

上传文件不建议放在 Web 容器可直接访问的静态目录下。更稳妥的方式是存储到对象存储、独立文件服务,或应用进程不可执行的本地目录,并通过受控接口下载。

Path baseDir = Paths.get("/data/app/uploads").toAbsolutePath().normalize();
String ext = getSafeExt(originalName);
String safeName = UUID.randomUUID() + "." + ext;
Path target = baseDir.resolve(safeName).normalize();

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

Files.copy(inputStream, target, StandardCopyOption.REPLACE_EXISTING);

4. 限制大小与数量

大小限制需要在多层配置中同时关注,包括网关、应用框架和业务逻辑。只在前端限制没有意义。

# Nginx 示例
client_max_body_size 10m;
# Spring Boot 示例
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=20MB

如果是多文件上传,还要限制单次上传数量、用户每日上传总量,并对异常频率做告警。

5. 下载接口做鉴权和安全响应头

如果文件需要被访问,建议通过下载接口进行鉴权,而不是直接暴露路径。响应时尽量使用附件下载方式,避免浏览器直接解释内容。

Content-Type: application/octet-stream
Content-Disposition: attachment; filename="download.bin"
X-Content-Type-Options: nosniff

对图片预览场景,可以单独走缩略图服务,避免把原始上传文件直接暴露给浏览器。

6. 日志与审计

上传接口需要记录关键字段,便于后续排查:

  • 用户 ID、角色、来源 IP。
  • 原始文件名、服务端保存名。
  • 文件大小、识别出的文件类型。
  • 校验失败原因。
  • 下载访问记录。

日志里不要记录完整敏感内容,也不要把用户上传文件内容直接打到日志中。

代码审计检查清单

检查项关注点
鉴权上传接口是否必须登录,是否校验业务权限
类型校验是否只看后缀,是否校验文件头和内容结构
文件名是否使用随机名,是否避免路径拼接风险
存储位置是否位于 Web 可访问目录,是否具备执行权限
大小限制网关、框架、业务层是否都有明确限制
后续处理图片、PDF、压缩包解析是否存在组件风险
访问控制下载或预览是否校验权限
日志审计是否能追踪上传者、文件、时间和结果

容易忽略的细节

  • 反向代理限制不等于业务限制:Nginx 限制了大小,不代表应用内部就不用校验。
  • 对象存储也要控权限:不要为了方便把 Bucket 设置成公开读写。
  • 异步处理要隔离:缩略图、OCR、PDF 解析建议放在低权限环境中执行。
  • 异常信息不要外显:上传失败时不要返回服务端绝对路径、组件栈信息。
  • 临时文件也要清理:上传失败、中断、解析异常时都要处理临时文件残留。
上传安全的核心不是“禁止某些危险后缀”,而是把文件当作不可信输入处理:入口要控、内容要验、存储要隔离、访问要鉴权、行为要留痕。

总结

文件上传漏洞在审计中并不难发现,但要修得稳,需要从业务链路整体考虑。单纯加一个后缀黑名单,或者依赖前端限制,基本不能算有效修复。

比较可靠的做法是:服务端白名单校验、内容识别、随机重命名、非 Web 目录存储、受控下载、大小和频率限制、日志审计,再结合后续解析链路的隔离。这样即使某一层判断出现偏差,也不至于让风险直接扩大。

这类问题适合作为代码审计里的固定检查项。尤其是老系统、后台系统、内部工具,往往因为“只有内部人用”而放松限制,但实际风险并不会因此消失。

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

0篇意见

推荐意见

没有意见。

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

SiteMap HACKHAT © 2026 All rights reserved.