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";
}

漏洞点分析

  1. 无文件后缀名过滤:可以上传任意后缀文件(如.jsp、.php等)
  2. 无内容类型检查:可以伪造MIME类型
  3. 存在目录遍历漏洞:通过../可实现任意目录写入
  4. 无文件内容验证:可以上传伪装成图片的恶意文件

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. 其他安全建议

  1. 文件存储隔离

    • 将上传文件存储在Web根目录外
    • 使用单独域名或子域名
    • 设置适当的文件权限
  2. 文件访问控制

    • 对上传的文件设置不可执行权限
    • 使用Nginx等限制特定目录的文件执行
  3. 日志记录

    • 记录上传操作,包括文件名、大小、上传者IP等
    • 对可疑上传行为进行监控
  4. 文件大小限制

    • 限制单个文件和总上传大小
    • 防止DoS攻击
  5. 病毒扫描

    • 对上传文件进行病毒扫描
    • 可使用ClamAV等开源工具

7. 总结

安全文件上传需要多层防御:

  1. 前端验证(用户体验,不可靠)
  2. 文件扩展名白名单
  3. MIME类型检查
  4. 文件内容验证
  5. 安全路径处理
  6. 文件重命名
  7. 适当的存储和访问控制

任何单一防护措施都可能被绕过,必须实施纵深防御策略才能有效保障文件上传功能的安全性。

Java代码审计之文件上传漏洞分析与防御 1. 文件上传漏洞概述 文件上传功能是Web应用中常见的功能,但如果实现不当,可能导致严重的安全问题。攻击者可能利用文件上传漏洞上传恶意文件(如Webshell),进而控制服务器。 2. 漏洞代码示例分析 2.1 基础漏洞示例 漏洞点分析 : 无文件后缀名过滤:可以上传任意后缀文件(如.jsp、.php等) 无内容类型检查:可以伪造MIME类型 存在目录遍历漏洞:通过 ../ 可实现任意目录写入 无文件内容验证:可以上传伪装成图片的恶意文件 2.2 目录遍历漏洞 攻击者可构造文件名如 ../../../var/www/html/shell.jsp ,将文件写入非预期目录。 3. 安全防护措施 3.1 文件后缀名白名单 注意事项 : 使用白名单而非黑名单 统一转换为小写比较,避免大小写绕过 获取后缀时确保文件名包含扩展名 3.2 MIME类型检查 注意事项 : MIME类型可被伪造,不能单独依赖 应结合其他验证方式 3.3 路径遍历防护 3.4 文件内容验证 注意事项 : 即使通过后缀和MIME检查,仍需验证实际内容 对于图片,可使用ImageIO验证 对于其他文件类型,需相应验证逻辑 4. 综合安全上传示例 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类型检查 文件内容验证 安全路径处理 文件重命名 适当的存储和访问控制 任何单一防护措施都可能被绕过,必须实施纵深防御策略才能有效保障文件上传功能的安全性。