🔍 文件上传漏洞Java源码审计详解(附代码分析)
文件上传是 Web 应用中极其常见的功能,但一旦实现不当,极易造成严重漏洞,如:上传 WebShell、任意文件写入、远程命令执行等。本篇将从源码审计角度,深入剖析文件上传中关键风险点,包含路径处理、文件大小限制、后缀校验、绕过技巧、白名单误用等,并提供典型实现方式与安全建议。
🧱 文件上传的常见实现方式
✅ 1. Spring MultipartFile 实现
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
String fileName = file.getOriginalFilename();
File dest = new File("/upload/dir/" + fileName);
file.transferTo(dest);
return "上传成功";
}
🔍 问题点分析:
⚠️ 2. 使用 Apache Commons FileUpload(ServletFileUpload)
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (!item.isFormField()) {
String fileName = item.getName();
File file = new File("/upload/" + fileName);
item.write(file);
}
}
🔍 风险分析与绕过技巧:
item.getName()
可被伪造,返回值可能为:../../webapps/ROOT/shell.jsp
。- 若直接写入本地文件系统,无适当处理,可能形成 RCE。
- Commons FileUpload 不自带后缀检查,全部靠开发人员自己处理。
☠️ 核心攻击点分析
📁 1. 路径遍历(Path Traversal)
❌ 不安全代码:
String fileName = request.getParameter("fileName");
File file = new File("/upload/" + fileName);
攻击示例:
fileName=../../../../webapps/ROOT/shell.jsp
🧨 效果: 上传的文件可能被写入 Web项目根目录 下,造成远程代码执行。
✅ 安全建议:
- 禁止文件名中包含
../
、\
、空格、%编码字符等。 - 使用
file.getCanonicalPath()
与上传目录前缀比对。
File dest = new File(uploadDir, fileName);
String canonicalPath = dest.getCanonicalPath();
if (!canonicalPath.startsWith(uploadDir.getCanonicalPath())) {
throw new SecurityException("路径非法"); //会判断上传文件的目录是否在合法目录
}
📏 2. 文件大小未限制(DoS 风险)
未设置上传大小限制,攻击者可构造超大文件导致内存/磁盘耗尽。
✅ Spring Boot 配置:
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 20MB
💣 3.ZIP 炸弹攻击分析
ZIP 炸弹是一种特制的压缩文件:
- 常用于攻击文件上传和解压服务,使 CPU、内存或磁盘瞬间耗尽
📉 攻击形式举例:
攻击者可能上传 20KB.zip
,但解压后文件达到 10GB,导致:
🔒 ZIP 炸弹防护建议
✅ 服务端代码解压前,务必加上以下限制:
🧪 Java 安全解压 ZIP 示例(带限制)
import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
publicclass SafeZipExtractor {
privatestaticfinallong MAX_TOTAL_UNZIPPED_SIZE = 100 * 1024 * 1024; // 100MB
privatestaticfinallong MAX_SINGLE_FILE_SIZE = 50 * 1024 * 1024; // 50MB
privatestaticfinalint MAX_FILE_COUNT = 100;
public static void unzipSafely(File zipFile, File targetDir) throws IOException {
long totalUnzippedSize = 0;
int fileCount = 0;
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
fileCount++;
if (fileCount > MAX_FILE_COUNT) {
thrownew SecurityException("解压文件数过多,疑似 ZIP 炸弹");
}
File newFile = new File(targetDir, entry.getName()).getCanonicalFile();
// 防止路径穿越
if (!newFile.getPath().startsWith(targetDir.getCanonicalPath())) {
thrownew SecurityException("非法路径,疑似穿越攻击: " + entry.getName());
}
// 逐步写出解压文件
try (FileOutputStream fos = new FileOutputStream(newFile)) {
byte[] buffer = newbyte[4096];
int len;
long singleFileSize = 0;
while ((len = zis.read(buffer)) > 0) {
singleFileSize += len;
totalUnzippedSize += len;
if (singleFileSize > MAX_SINGLE_FILE_SIZE) {
thrownew SecurityException("单个文件过大,疑似 ZIP 炸弹");
}
if (totalUnzippedSize > MAX_TOTAL_UNZIPPED_SIZE) {
thrownew SecurityException("解压内容总体积过大,疑似 ZIP 炸弹");
}
fos.write(buffer, 0, len);
}
}
}
}
}
}
✅ 上面防护总结:
🚨 其他方式:
- 使用像 Zip4j 或 Apache Commons Compress 这样的库能获得更多安全控制。
📂 4. 后缀名校验缺失或被绕过
❌ 错误做法:黑名单
if (fileName.endsWith(".jsp") || fileName.endsWith(".php")) {
throw new SecurityException("禁止上传脚本文件");
}
🧨 绕过方式:
| |
---|
| shell.jpg.jsp |
| shell.JSP |
| shell.jsp%00.jpg |
✅ 正确做法:白名单校验
List<String> allowExt = Arrays.asList(".jpg", ".png", ".pdf", ".docx");
String ext = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
//.的定位也很重要,如果用的不是lastIndexOf定位最后一个.大概率存在问题
if (!allowExt.contains(ext)) {
throw new SecurityException("非法文件类型");
}
🎭 5. 文件名中“点”的位置及隐藏文件攻击
攻击者可上传如下文件名:
🧨 某些系统识别后缀可能失误,或将其当作隐藏文件,或绕过后缀判断。
✅ 建议:
if (fileName.startsWith(".") || fileName.contains("..") || fileName.trim().endsWith(".")) {
throw new SecurityException("非法文件名");
}
🔒 推荐安全上传实现
@PostMapping("/upload")
public String secureUpload(@RequestParam("file") MultipartFile file) throws IOException {
String originalName = file.getOriginalFilename();
// 后缀白名单
String suffix = originalName.substring(originalName.lastIndexOf(".")).toLowerCase();
List<String> allow = Arrays.asList(".jpg", ".png", ".pdf");
if (!allow.contains(suffix)) {
thrownew IllegalArgumentException("不允许的文件类型");
}
// 随机命名 + 限定目录
String newName = UUID.randomUUID().toString().replace("-", "") + suffix;
File saveDir = new File("/opt/upload/");
if (!saveDir.exists()) saveDir.mkdirs();
File dest = new File(saveDir, newName);
// 路径校验
if (!dest.getCanonicalPath().startsWith(saveDir.getCanonicalPath())) {
thrownew SecurityException("非法路径");
}
file.transferTo(dest);
return"上传成功";
}
🛡️ 审计 Checklist
📌 文件上传总结
| | |
---|
路径控制 | - 路径穿越 ../ - 绝对路径注入 - 上传符号链接 | - 使用 getCanonicalPath() 规范化路径 - 校验目标路径是否在允许目录内 - 禁止软链接上传 |
文件类型绕过 | - 双扩展 .php.jpg - 特殊字符 .php%00.jpg - 控制字符/Unicode 后缀绕过 - 魔术头伪装 | - 使用魔术头/MIME 检测文件内容 - 白名单方式验证扩展名 - 禁止解析上传目录(如配置 nginx/php) |
ZIP炸弹 | - 高压缩比 ZIP - 多层嵌套压缩包 - 超大文件数 | - 限制最大解压大小、文件数、嵌套层数 - 使用 Zip4j/Apache Commons 解压时封装限制 - 拒绝可疑压缩比(如 >1000:1) |
图片马 | - 上传带 PHP 代码的图片 - 利用图片解析漏洞 | - 不允许上传至可执行目录 - 拒绝上传含脚本内容的图像(验证内容头) |
WAF绕过/编码绕过 | - 双写编码绕过 - Content-Type 伪造 - 分块传输绕过检测 | - 上传前验证 MIME 与扩展是否匹配 - 服务端统一校验,不信任前端类型信息 - 使用 RASP 或反向代理层识别异常流量 |
大文件/并发DoS | - 上传超大文件压垮服务器 - 并发上传大量小文件耗尽 inode | - 限制 max-file-size / max-request-size - 控制上传频率(限流) - 后台作业处理上传文件 |
上传后可访问 | - 上传后直接访问执行 shell - 前后端路径绕过 | - 上传文件重命名为随机 UUID - 上传目录配置为不可执行 - 存储层与访问层解耦 |
阅读原文:原文链接
该文章在 2025/4/18 11:55:38 编辑过