跳转到帖子

背景

文件上传是 Web 安全里很常见的风险点,但真正做代码审计时,问题往往不在“有没有上传接口”这么简单,而是出现在校验链路不一致、存储位置不合理、解析行为被忽略、权限边界缺失等细节上。

这篇文章整理一次典型文件上传缺陷的复盘思路,重点放在如何审计、如何验证风险、如何修复和加固。内容只讨论授权环境下的安全测试与代码审计,不涉及未授权攻击或恶意利用。

典型场景

某业务系统提供附件上传功能,用于上传头像、工单附件、富文本图片等。系统后端通常会做以下限制:

  • 限制文件后缀,例如只允许 jpg、png、pdf、docx。
  • 限制 Content-Type,例如 image/jpeg、image/png。
  • 限制文件大小。
  • 将文件保存到固定目录。
  • 返回文件访问 URL,供前端展示或下载。

表面看这些措施已经比较完整,但在审计中经常能看到几个问题:只校验客户端传来的文件名、只校验 Content-Type、不校验真实文件内容、上传目录可执行、文件名可控、下载接口缺少权限判断。

问题拆解

文件上传风险可以按链路拆成四段来看:接收、校验、存储、访问。

阶段常见问题审计重点
接收信任前端参数、未限制大小Multipart 解析、字段来源、异常处理
校验只校验后缀或 Content-Type白名单、Magic Number、文件结构检测
存储原文件名落盘、目录可执行随机文件名、路径拼接、安全目录
访问公开直链、越权下载鉴权、引用关系、访问控制

代码审计切入点

实际审计时,不建议直接全局搜索“upload”就结束。可以结合路由、控制器、文件处理库、对象存储 SDK、下载接口一起看。

常用检索关键字包括:

upload
multipart
MultipartFile
getOriginalFilename
transferTo
move_uploaded_file
FileOutputStream
Files.copy
Content-Type
attachment
oss
cos
minio
download

以 Java Web 项目为例,看到类似代码时需要重点关注:

String fileName = file.getOriginalFilename();
String ext = fileName.substring(fileName.lastIndexOf("."));
if (!allowExt.contains(ext)) {
    throw new RuntimeException("invalid file type");
}
File dest = new File(uploadDir + "/" + fileName);
file.transferTo(dest);

这段代码的问题比较典型:

  • 使用原始文件名,可能存在重名覆盖、特殊字符、路径拼接风险。
  • 只通过后缀判断类型,无法保证文件内容真实可信。
  • 上传目录如果被 Web 容器直接解析,风险会进一步放大。
  • 没有看到权限、大小、内容检测、异常清理等逻辑。

验证思路:重点看风险是否可达

在授权测试环境里,验证文件上传问题时,不建议只停留在“能上传一个异常后缀文件”。真正需要确认的是风险路径是否闭环。

  • 异常文件是否能成功保存。
  • 保存位置是否在 Web 可访问目录下。
  • 访问时是下载还是被服务器解析。
  • 文件 URL 是否可被未授权访问。
  • 不同业务用户之间是否能访问彼此文件。
  • 上传失败后是否留下临时文件。

可以使用无害的文本内容做验证,例如上传一个包含固定字符串的测试文件,确认系统是否按预期拒绝、重命名、隔离或下载。

SECURITY_UPLOAD_TEST_2026
This is a harmless test file for authorized validation.

如果系统声明只允许图片,可以准备一张正常图片,并检查服务端是否真正解析图片结构,而不是只依赖文件名和请求头。对于图片类文件,建议后端读取并重新编码输出,避免把原始内容直接保存为可访问资源。

更可靠的校验方式

上传校验建议采用多层白名单,而不是单点判断。

  • 后缀白名单:只允许明确业务需要的类型。
  • MIME 类型检测:作为辅助判断,不作为唯一依据。
  • Magic Number 检测:识别常见文件头。
  • 文件结构解析:图片使用图片库读取,PDF、Office 文档使用成熟库做基础解析。
  • 文件大小限制:按业务场景设置上限,并限制压缩包解压行为。
  • 文件名重写:服务端生成随机文件名,不信任用户传入的名称。

示例:服务端生成安全文件名的思路。

String originalName = file.getOriginalFilename();
String ext = getSafeExtension(originalName);

if (!ALLOWED_EXTENSIONS.contains(ext)) {
    throw new IllegalArgumentException("unsupported file type");
}

String safeName = UUID.randomUUID().toString().replace("-", "") + ext;
Path target = Paths.get(uploadBaseDir).resolve(safeName).normalize();

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

Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);

这里有两个关键点:一是文件名完全由服务端生成;二是路径 normalize 后要确认仍在预期目录内,避免路径拼接带来的边界问题。

存储与访问控制

很多上传问题最后变成安全事件,并不是因为上传本身,而是因为上传后的访问控制做得太粗糙。

推荐做法:

  • 上传目录不要放在 Web 根目录下。
  • 静态资源服务器禁用脚本解析,只按下载内容处理。
  • 对象存储默认私有,访问时通过后端鉴权后生成短期有效 URL。
  • 文件记录与业务对象绑定,例如工单 ID、用户 ID、租户 ID。
  • 下载接口必须校验当前用户是否有权访问对应业务对象。
  • 对敏感文件增加审计日志,包括上传者、下载者、时间、业务对象。

一个常见的下载接口反例是只根据 fileId 查询文件路径,然后直接返回:

GET /api/file/download?id=12345

如果后端没有校验 fileId 与当前用户、当前租户或当前业务记录的关系,就容易出现越权下载。正确做法应该是把文件访问纳入业务权限模型,而不是单独把文件当作公共资源。

Nginx 层面的基础加固

如果业务需要通过 Nginx 提供附件下载,应确保上传目录不会执行脚本。以下配置仅作为思路示例,实际路径和策略需结合环境调整。

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

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

如果是图片展示场景,也建议加上 nosniff,减少浏览器根据内容嗅探带来的额外风险。

日志与告警

上传接口非常适合做安全审计日志,因为异常行为通常比较明显。

  • 记录上传文件大小、后缀、检测结果、业务 ID、用户 ID。
  • 对连续上传失败、异常后缀、超大文件、压缩包异常结构做告警。
  • 记录文件访问日志,尤其是跨租户、批量下载、短时间高频下载。
  • 上传失败时注意清理临时文件,避免磁盘被打满。

日志不要记录文件完整内容,也不要把敏感文件名、身份证号等直接打到普通日志里。可以记录哈希、文件 ID 和风险标签。

代码审计清单

做文件上传相关审计时,我通常会按下面清单过一遍:

  • 是否所有上传入口都走统一组件。
  • 是否存在绕过统一组件的历史接口。
  • 是否只依赖前端限制。
  • 是否只判断 Content-Type 或后缀。
  • 是否使用用户原始文件名落盘。
  • 是否存在路径拼接和目录穿越风险。
  • 上传目录是否可被 Web 容器解析。
  • 对象存储 Bucket 是否公开。
  • 文件下载是否校验业务权限。
  • 是否限制文件大小和上传频率。
  • 压缩包是否有解压逻辑,是否限制解压层级、总大小和文件数量。
  • 失败分支是否清理临时文件。
  • 是否有上传与下载审计日志。

修复优先级建议

如果系统已经上线,建议按风险可达性排序,而不是一次性大改所有逻辑。

  • 第一优先级:上传目录可执行、公开访问、权限缺失的问题。
  • 第二优先级:文件类型校验薄弱、原文件名落盘、路径拼接问题。
  • 第三优先级:日志、告警、频率限制、异常文件清理。
  • 第四优先级:历史文件迁移、存储架构调整、统一文件服务改造。

修复时要注意兼容历史数据。例如原来公开访问的附件改为私有访问后,前端展示、富文本图片、移动端缓存都可能受影响,需要先梳理业务引用关系。

总结

文件上传安全不是单个 if 判断能解决的问题。比较稳妥的做法是把上传当成一条完整链路:入口统一、类型严格、文件名重写、目录隔离、访问鉴权、日志可追溯。

从代码审计角度看,最有价值的不是找到一个“能上传异常文件”的点,而是判断它能否被访问、能否被解析、是否越权、是否影响真实业务边界。只有把这些问题串起来,风险评级和修复建议才会比较准确,也更容易被研发团队接受和落地。

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

0篇意见

推荐意见

没有意见。

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