跳转到帖子

背景

文件上传是 Web 系统里非常常见的功能,头像、附件、工单截图、导入模板都会涉及。它看起来只是业务能力,但在安全审计里经常是高风险入口:一旦校验逻辑不完整,可能引发任意文件写入、存储型 XSS、服务端解析异常、敏感文件覆盖等问题。

这篇文章整理一次比较典型的文件上传风险排查思路,重点放在代码审计和防护设计上。内容不会涉及未授权入侵或攻击利用细节,更多是从研发和安全评审角度,说明如何判断一个上传功能是否可靠。

常见场景

在实际项目里,文件上传通常存在几类实现方式:

  • 后端应用直接接收文件并保存到本地目录。
  • 后端接收后转存到对象存储,例如 S3、OSS、COS、MinIO。
  • 前端直传对象存储,后端只负责签发上传凭证。
  • 上传后进入异步处理流程,例如图片压缩、文档解析、病毒扫描。

不同实现方式风险点不完全一样,但核心问题基本围绕三个方面:文件内容是否可信、文件路径是否可控、文件访问是否安全。

问题分析

审计上传功能时,不建议只看扩展名判断。扩展名是用户可控输入,只能作为辅助信息。更稳妥的方式是从以下几个维度交叉验证。

一、文件类型校验不能只依赖后缀

很多历史代码会这样判断:

String filename = file.getOriginalFilename();
if (filename.endsWith(".jpg") || filename.endsWith(".png")) {
    file.transferTo(new File(uploadDir, filename));
}

这个逻辑的问题比较明显:文件名完全来自客户端,攻击者可以构造特殊名称;同时,后缀无法证明文件内容是真正的图片。

更可靠的做法是至少同时检查 MIME、文件头特征和实际解码结果。以图片为例,可以用标准图片库尝试解析,并限制宽高、大小、格式。

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

int width = image.getWidth();
int height = image.getHeight();
if (width <= 0 || height <= 0 || width > 8000 || height > 8000) {
    throw new IllegalArgumentException("invalid image size");
}

注意,MIME 头也来自请求,不应单独作为信任依据。它的价值更多是辅助判断和记录日志。

二、文件名和路径必须重新生成

上传保存时,最容易被忽略的是路径拼接。以下写法在代码审计中比较常见:

File target = new File(uploadDir + "/" + file.getOriginalFilename());
file.transferTo(target);

这里存在两个问题:一是原始文件名可能包含路径分隔符;二是可能导致文件覆盖。安全做法是服务端生成随机文件名,并将用户原始文件名仅作为展示字段保存到数据库。

String ext = detectSafeExtension(file);
String safeName = UUID.randomUUID().toString().replace("-", "") + ext;
Path basePath = Paths.get(uploadDir).toAbsolutePath().normalize();
Path targetPath = basePath.resolve(safeName).normalize();

if (!targetPath.startsWith(basePath)) {
    throw new IllegalArgumentException("invalid upload path");
}

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

这里的关键点是 normalize 后再检查 startsWith,避免路径穿越导致文件落到预期目录之外。

三、上传目录不要具备脚本执行能力

如果上传目录位于 Web 根目录下,并且应用服务器会解析其中某些脚本类型,那么风险会被放大。更推荐的方式是:

  • 上传文件存储在 Web 根目录之外。
  • 通过受控下载接口读取文件。
  • 对象存储桶默认私有,访问通过短期签名 URL 或后端代理。
  • 静态资源域名与主站域名隔离,降低 XSS 影响面。

Nginx 可以显式禁止上传目录下的脚本解析。示例仅作思路参考:

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

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

如果业务允许公开访问图片,也建议设置准确的 Content-Type,并加上 X-Content-Type-Options: nosniff,避免浏览器进行内容嗅探。

四、大小、数量和解压类文件要做限制

上传功能不仅有代码执行类风险,也有资源消耗风险。例如超大文件、批量上传、压缩包炸弹、图片解析消耗过高等。实际防护里建议加入:

  • 单文件大小限制。
  • 单用户或单 IP 上传频率限制。
  • 总存储配额限制。
  • 图片宽高和像素总量限制。
  • 压缩包解压后的文件数量、总大小、目录层级限制。

对于 ZIP 处理,不能简单相信压缩前大小。需要边解压边统计,超过阈值立刻中断。

long totalSize = 0;
int fileCount = 0;

while ((entry = zipInputStream.getNextEntry()) != null) {
    if (entry.isDirectory()) {
        continue;
    }
    fileCount++;
    if (fileCount > 1000) {
        throw new IllegalArgumentException("too many files");
    }

    long written = copyWithLimit(zipInputStream, outputStream, 10 * 1024 * 1024);
    totalSize += written;
    if (totalSize > 100 * 1024 * 1024) {
        throw new IllegalArgumentException("zip too large");
    }
}

五、文件下载接口也要一起审计

很多时候上传保存本身没有明显问题,但下载接口存在越权或任意文件读取。常见风险包括:

  • 通过 fileId 下载时没有校验当前用户权限。
  • 通过 path 参数读取文件,导致路径穿越。
  • 返回文件时 Content-Disposition 处理不当,引发响应头注入。
  • 私有文件使用永久公开 URL,无法撤销访问。

较稳妥的做法是下载接口只接收文件 ID,通过数据库查询真实存储路径,并校验业务权限。

Attachment attachment = attachmentService.getById(fileId);
if (attachment == null || !permissionService.canRead(currentUser, attachment)) {
    throw new AccessDeniedException("no permission");
}

Path filePath = storage.resolve(attachment.getStorageName());
return download(filePath, attachment.getOriginalName());

关键审计步骤

实际排查时,可以按下面的顺序进行,效率会比较高。

  • 定位所有上传入口,包括后台、接口、富文本、导入功能和移动端接口。
  • 确认文件最终保存位置,是本地目录还是对象存储。
  • 检查文件名是否由服务端生成,是否存在路径拼接。
  • 检查类型校验是否只依赖后缀或 Content-Type。
  • 检查上传目录是否可被 Web 服务器解析执行。
  • 检查是否存在大小、频率、配额限制。
  • 检查下载接口是否有权限校验和路径约束。
  • 查看日志中是否记录上传人、文件 ID、大小、类型、存储路径、处理结果。

安全基线建议

检查项建议做法
文件名服务端随机生成,原始文件名仅用于展示
保存路径固定目录,normalize 后校验路径边界
类型判断后缀、文件头、解析结果多重校验
访问控制私有文件必须鉴权,避免永久公开链接
执行权限上传目录禁止脚本执行,尽量放在 Web 根目录外
资源限制限制大小、数量、频率、解压后总量
日志审计记录上传主体、文件信息、处理结果和异常原因

注意点

上传安全不是一个 if 判断能解决的问题,而是一组边界控制:入口校验、存储隔离、访问鉴权、解析限制和日志审计都要同时考虑。

  • 不要把前端校验当作安全措施,前端只能改善用户体验。
  • 不要信任原始文件名、MIME、后缀、压缩包内路径。
  • 不要让上传文件和应用代码处在同一执行上下文。
  • 涉及文档预览、图片处理时,要关注第三方解析库漏洞和补丁更新。
  • 如果有杀毒或内容检测流程,建议异步处理并设置隔离状态,未检测通过前不要公开访问。

总结

文件上传风险的根源在于“把不可信内容带入服务端存储和解析流程”。审计时不能只看能不能上传某个后缀,更要看文件从进入系统到被访问、被解析、被下载的完整链路。

比较可靠的方案是:服务端生成文件名,固定存储目录,做内容级校验,上传目录不具备执行能力,下载接口按业务权限控制,并对资源消耗做上限约束。这样即使某个单点校验出现遗漏,也不会轻易演变成高危问题。

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

0篇意见

推荐意见

没有意见。

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