JAVA代码审计之文件上传
字数 1112 2025-08-12 11:34:29
Java代码审计之文件上传漏洞分析与防御
1. 文件上传漏洞概述
文件上传功能是Web应用中常见的功能,但如果实现不当,可能导致严重的安全问题。攻击者可能利用文件上传漏洞上传恶意文件(如Webshell),进而控制服务器。
2. 漏洞代码示例分析
2.1 基础漏洞示例
@PostMapping("/upload")
public String singleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
if (file.isEmpty()) {
redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
return "redirect:/file/status";
}
// 直接保存文件,无任何过滤
byte[] bytes = file.getBytes();
Path path = Paths.get("/tmp/" + file.getOriginalFilename());
Files.write(path, bytes);
redirectAttributes.addFlashAttribute("message", "Upload success");
return "redirect:/file/status";
}
漏洞点分析:
- 无文件后缀名过滤:可以上传任意后缀文件(如.jsp、.php等)
- 无内容类型检查:可以伪造MIME类型
- 存在目录遍历漏洞:通过
../可实现任意目录写入 - 无文件内容验证:可以上传伪装成图片的恶意文件
2.2 目录遍历漏洞
Path path = Paths.get("/tmp/" + file.getOriginalFilename());
攻击者可构造文件名如../../../var/www/html/shell.jsp,将文件写入非预期目录。
3. 安全防护措施
3.1 文件后缀名白名单
// 白名单图片后缀
String[] picSuffixList = {".jpg", ".png", ".jpeg", ".gif", ".bmp", ".ico"};
boolean suffixFlag = false;
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
for (String white_suffix : picSuffixList) {
if (suffix.equals(white_suffix)) {
suffixFlag = true;
break;
}
}
if (!suffixFlag) {
return "Upload failed. Illegal file type.";
}
注意事项:
- 使用白名单而非黑名单
- 统一转换为小写比较,避免大小写绕过
- 获取后缀时确保文件名包含扩展名
3.2 MIME类型检查
// 黑名单危险MIME类型
String[] mimeTypeBlackList = {
"text/html",
"text/javascript",
"application/javascript",
"application/ecmascript",
"text/xml",
"application/xml"
};
String mimeType = file.getContentType();
for (String blackMimeType : mimeTypeBlackList) {
// 使用contains防止类似text/html;charset=UTF-8的绕过
if (mimeType != null && mimeType.toLowerCase().contains(blackMimeType)) {
return "Upload failed. Illegal file type.";
}
}
注意事项:
- MIME类型可被伪造,不能单独依赖
- 应结合其他验证方式
3.3 路径遍历防护
// 使用UUID重命名文件
String uuid = UUID.randomUUID().toString();
String safeFileName = uuid + suffix;
// 使用File对象的toPath()方法避免路径拼接
File destFile = new File("/upload", safeFileName);
Path path = destFile.toPath();
// 或者使用规范化路径检查
Path uploadPath = Paths.get("/upload").normalize();
Path resolvedPath = uploadPath.resolve(file.getOriginalFilename()).normalize();
if (!resolvedPath.startsWith(uploadPath)) {
// 检测到路径遍历
return "Upload failed. Illegal file path.";
}
3.4 文件内容验证
private static boolean isImage(File file) throws IOException {
BufferedImage bi = ImageIO.read(file);
return bi != null;
}
// 使用示例
if (!isImage(uploadedFile)) {
deleteFile(filePath);
return "Upload failed. File is not a valid image.";
}
注意事项:
- 即使通过后缀和MIME检查,仍需验证实际内容
- 对于图片,可使用ImageIO验证
- 对于其他文件类型,需相应验证逻辑
4. 综合安全上传示例
@PostMapping("/secureUpload")
public String secureFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
try {
// 1. 检查文件是否为空
if (file.isEmpty()) {
redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
return "redirect:/file/status";
}
// 2. 检查文件后缀名
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || !originalFilename.contains(".")) {
return "Upload failed. Invalid filename.";
}
String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
String[] allowedExtensions = {".jpg", ".png", ".jpeg"};
if (!Arrays.asList(allowedExtensions).contains(suffix)) {
return "Upload failed. File type not allowed.";
}
// 3. 检查MIME类型
String mimeType = file.getContentType();
String[] blacklistedMimeTypes = {"text/html", "application/x-php"};
for (String blackType : blacklistedMimeTypes) {
if (mimeType != null && mimeType.toLowerCase().contains(blackType)) {
return "Upload failed. Illegal file type.";
}
}
// 4. 安全存储文件名
String safeFileName = UUID.randomUUID().toString() + suffix;
File destFile = new File("/secure/upload/dir", safeFileName);
// 5. 检查路径遍历
Path uploadPath = Paths.get("/secure/upload/dir").toAbsolutePath().normalize();
Path destPath = destFile.toPath().toAbsolutePath().normalize();
if (!destPath.startsWith(uploadPath)) {
return "Upload failed. Illegal file path.";
}
// 6. 保存文件
file.transferTo(destPath);
// 7. 验证文件内容
if (!isImage(destFile)) {
Files.deleteIfExists(destPath);
return "Upload failed. File content verification failed.";
}
redirectAttributes.addFlashAttribute("message", "Upload success");
return "redirect:/file/status";
} catch (IOException e) {
return "Upload failed. Server error.";
}
}
5. 常见绕过手段及防御
5.1 双扩展名绕过
- 攻击方式:
shell.jpg.php - 防御:获取最后一个
.后的扩展名
5.2 大小写绕过
- 攻击方式:
shell.JPG - 防御:统一转换为小写比较
5.3 空字节截断
- 攻击方式:
shell.php%00.jpg - 防御:Java较新版本已修复此问题,但仍需注意
5.4 MIME伪造
- 攻击方式:修改Content-Type头
- 防御:结合文件内容验证
5.5 图片马
- 攻击方式:在图片中嵌入恶意代码
- 防御:使用ImageIO验证图片完整性
6. 其他安全建议
-
文件存储隔离:
- 将上传文件存储在Web根目录外
- 使用单独域名或子域名
- 设置适当的文件权限
-
文件访问控制:
- 对上传的文件设置不可执行权限
- 使用Nginx等限制特定目录的文件执行
-
日志记录:
- 记录上传操作,包括文件名、大小、上传者IP等
- 对可疑上传行为进行监控
-
文件大小限制:
- 限制单个文件和总上传大小
- 防止DoS攻击
-
病毒扫描:
- 对上传文件进行病毒扫描
- 可使用ClamAV等开源工具
7. 总结
安全文件上传需要多层防御:
- 前端验证(用户体验,不可靠)
- 文件扩展名白名单
- MIME类型检查
- 文件内容验证
- 安全路径处理
- 文件重命名
- 适当的存储和访问控制
任何单一防护措施都可能被绕过,必须实施纵深防御策略才能有效保障文件上传功能的安全性。