🔍 文件上传漏洞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"上传成功";}
🔍 问题点分析:
|
|
---|---|
fileName |
../ |
|
|
|
.jsp 等恶意脚本 |
|
|
|
|
⚠️ 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())) {thrownew SecurityException("路径非法"); //会判断上传文件的目录是否在合法目录}
📏 2. 文件大小未限制(DoS 风险)
未设置上传大小限制,攻击者可构造超大文件导致内存/磁盘耗尽。
✅ Spring Boot 配置:
spring:servlet:multipart:max-file-size:10MBmax-request-size:20MB
💣 3.ZIP 炸弹攻击分析
ZIP 炸弹是一种特制的压缩文件:
-
文件体积非常小(几十 KB) -
解压后数据极大(几个 GB 到 TB) -
常用于攻击文件上传和解压服务,使 CPU、内存或磁盘瞬间耗尽
📉 攻击形式举例:
|
|
|
---|---|---|
|
|
|
攻击者可能上传 20KB.zip
,但解压后文件达到 10GB,导致:
-
磁盘被写满 -
内存耗尽或服务器卡死 -
服务完全崩溃
🔒 ZIP 炸弹防护建议
✅ 服务端代码解压前,务必加上以下限制:
-
限制解压后文件总大小 -
限制单个文件大小 -
限制文件数量 -
检测压缩比(压缩比过高直接拒绝)
🧪 Java 安全解压 ZIP 示例(带限制)
import java.io.*;import java.util.zip.ZipEntry;import java.util.zip.ZipInputStream;publicclassSafeZipExtractor{privatestaticfinallong MAX_TOTAL_UNZIPPED_SIZE = 100 * 1024 * 1024; // 100MBprivatestaticfinallong MAX_SINGLE_FILE_SIZE = 50 * 1024 * 1024; // 50MBprivatestaticfinalint MAX_FILE_COUNT = 100;publicstaticvoidunzipSafely(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")) {thrownew 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)) {thrownew SecurityException("非法文件类型");}
🎭 5. 文件名中“点”的位置及隐藏文件攻击
攻击者可上传如下文件名:
-
.htaccess
-
.env
-
.ssh/authorized_keys
-
abc.jsp.
(带空格)
🧨 某些系统识别后缀可能失误,或将其当作隐藏文件,或绕过后缀判断。
✅ 建议:
-
拒绝以点开头的文件(隐藏文件) -
拒绝多个点或尾部空格(如 file.jsp
)
if (fileName.startsWith(".") || fileName.contains("..") || fileName.trim().endsWith(".")) {thrownew 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
|
|
|
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.xxx
|
📌 文件上传总结
|
|
|
---|---|---|
路径控制 |
../ - 绝对路径注入- 上传符号链接 |
getCanonicalPath() 规范化路径- 校验目标路径是否在允许目录内- 禁止软链接上传 |
文件类型绕过 |
.php.jpg - 特殊字符 .php%00.jpg - 控制字符/Unicode 后缀绕过- 魔术头伪装 |
|
ZIP炸弹 |
|
|
图片马 |
|
|
WAF绕过/编码绕过 |
|
|
大文件/并发DoS |
|
max-file-size / max-request-size - 控制上传频率(限流)- 后台作业处理上传文件 |
上传后可访问 |
|
|
原文始发于微信公众号(季升安全):一文吃透文件上传漏洞!路径遍历、后缀绕过、ZIP炸弹全解析
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论