任意文件上传漏洞代码审计详解

1.成因

任意文件上传漏洞常发生在文件上传功能中,由于后端代码中没有严格限制用户上传的文件,导致攻击
者可以上传带有恶意攻击代码的脚本到目标服务器,进而执行脚本,以达到控制操纵目标服务器等目的

2.审计的步骤

在对文件上传功能进行代码审计时我们主要分析整个上传流程对所上传文件做了什么样的操作。进而分
析是否能够造成任意文件上传漏洞。
我们比较关注的几点:

①、SpringBoot对JSP的限制。

②、文件后缀名是否存在白名单。

③、文件类型是否存在白名单。

④、所保存的路径是否能够解析JSP。

⑤、文件头检测。

2.1 SpringBoot对JSP的限制

官方不建议 SpringBoot 使用 JSP, 并且对其做了限制,如果需要使用JSP 那么需要引入相关依赖,自建WEB-INF,web.xml等操作,这样一通操作下来失去了SpringBoot的一些特性

对其实施代码审计 直接观察pom.xml文件 查看对其写的组件

2.2 文件校验类型

2.2.1文件后缀名校验

这里我们主要关注后端是否对上传的文件后缀名进行了检查判断。如果在后端对后缀名没有限制。那么就极有可能存在任意文件上传漏洞

如果是前端进行了后缀名检测 ,我们只需要在BP中替换掉文件即可或者在前端中去掉限制相关的代码, 使其限制失效。

2.2.2 文件后缀名校验黑名单

黑名单验证虽然对防护有一定的作用, 如果黑名单存在遗漏,那么还是可以通过fuzz 进行绕过, 建议使用白名单验证,这样只有符合白名单里的后缀名才可以进行上传。

// 获取文件后缀名
String Suffix = fileName.substring(fileName.lastIndexOf(".")); 
//黑or白名单判断
String[] SuffixList = {".jpg", ".png", ".jpeg", ".gif"}

mime type校验

校验文件类型还有一种方式检测MIME Type。也就是我们在请求中常见的 Content-Type 字段。
如果项目中使用MIME type黑白名单检测文件类型,可以分析黑白名单中是否有遗漏的敏感文件类型。

常见的MIME类型

https://developer.mozilla.org/en-US/search?q=MIME%20types

不安全写法:

// 仅浏览器提供,可被伪造(如Burp改包)
String mime = file.getContentType(); // Spring MultipartFile
// 可能返回 "image/png"(即使实际是PHP脚本

安全写法:使用Java的URLConnection 或第三方库(如Apache Tika):

import java.net.URLConnection;
import org.apache.tika.Tika;

// 方法2.1:URLConnection(内置JDK)
InputStream is = file.getInputStream();
String mime = URLConnection.guessContentTypeFromStream(is); // 如 "image/jpeg"

// 方法2.2:Apache Tika(更精准,支持多种格式)
Tika tika = new Tika();
String mime = tika.detect(file.getInputStream()); // 如 "image/png"

2.2.3文件名操作

这里我们所关注的就是上传保存的文件名是否会被重命名

常见的场景是后端直接接收上传的文件名,但较为安全的做法是重新随机生成文件名 比如使用UUID

String originalFileName = file.getOriginalFilename();
String extension = originalFileName.substring(originalFileName.lastIndexOf('.'));
String fileName = UUID.randomUUID() + extension;

2.2.4 保存路径

关注是否保存在本地,保存文件路径是否是在非解析路径,保存路径是否可控。

String filename = request.getParameter("filename"); // 用户可控
File file = new File("/uploads/" + filename);       // 目录遍历风险
file.transferTo(new File("/var/www/uploads/" + filename)); // 覆盖风险

2.2.5 是否保存在本地

现在很多项目可以说是都在向云服务器迁移,并且对数据,文件做了隔离。不同的的场景应用不同的存储服务器。
后端在对上传文件保存时无非要么是保存在服务器本地,要么保存在相关云存储服务器。比如:七牛云
OSS,阿里OSS等等。如果保存在了OSS上,尽管上传了敏感Webshell脚本,也是无法执行的。

如果需要保存到本地 需要对文件夹进行保护 (防目录穿越)

import java.nio.file.Path;
import java.nio.file.Paths;

Path basePath = Paths.get("/opt/uploads").toAbsolutePath().normalize();
Path resolvedPath = basePath.resolve(filename).normalize();

// 检查是否在基目录内
if (!resolvedPath.startsWith(basePath)) {
    throw new SecurityException("Path traversal detected!");
}

2.2.6 是否解析路径

是将保存的文件保存在了 webapp/jsp 目录下,该目录可以解析 JSP 文件,进而造成了任意文件上传漏洞。
在实际项目中,所保存文件的地址可能是一个不可执行不可解析权限非常低的目录,尽管我们将WebShell上传到了目标服务器,那么也因无法解析执行而无功而返。

2.2.7文件上传功能的关键字

在面对一个完整的项目中有我常用以下方式定位上传功能,比如: 查看需求文档 , 查看Controller层 ,
部署后通过前端定位功能点 , 全局搜索关键字 等等

File
FileUpload
FileUploadBase
FileItemIteratorImpl
FileItemStreamImpl
FileUtils
UploadHandleServlet
FileLoadServlet
FileOutputStream
DiskFileItemFactory
MultipartRequestEntity
MultipartFile
com.oreilly.servlet.MultipartRequest

3.任意文件上传漏洞修复

列出允许的扩展。只允许业务功能的安全和关键扩展
确保在验证扩展名之前应用输入验证。
验证文件类型,不要相信Content-Type头,因为它可以被欺骗。
将文件名改为由应用程序生成的文件名
设置一个文件名的长度限制。如果可能的话,限制允许的字符
设置一个文件大小限制
只允许授权用户上传文件
将文件存储在不同的服务器上。如果不可能,就把它们存放在webroot之外。
在公众访问文件的情况下,使用一个处理程序,在应用程序中被映射到文件名(someid -> file.ext)。
通过杀毒软件或沙盒(如果有的话)运行文件,以验证它不包含恶意数据。
确保任何使用的库都是安全配置的,并保持最新。
保护文件上传免受CSRF攻击

标准的代码 ——java类

package com.demo.util;

import org.apache.commons.io.FilenameUtils;
import org.apache.tika.Tika;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.*;
import java.util.Set;
import java.util.UUID;

public final class FileUploadUtil {

    // 1. 允许的 MIME 白名单(可按业务扩展)
    private static final Set<String> ALLOW_MIME = Set.of(
            "image/jpeg", "image/png", "image/gif",
            "application/pdf"
    );

    // 2. 允许的扩展名白名单(与 MIME 对应)
    private static final Set<String> ALLOW_EXT = Set.of(
            "jpg", "jpeg", "png", "gif", "pdf"
    );

    private static final Tika TIKA = new Tika();

    private FileUploadUtil() {}

    /**
     * 安全保存文件并返回最终路径
     * @param baseDir 基目录(如 /opt/app/uploads)
     * @param file    MultipartFile
     * @return 相对路径,用于后续访问
     */
    public static String save(Path baseDir, MultipartFile file) throws IOException {
        // 1. 空文件校验
        if (file.isEmpty()) {
            throw new IllegalArgumentException("Empty file");
        }

        // 2. MIME 类型校验(基于文件内容)
        String mime = TIKA.detect(file.getInputStream());
        if (!ALLOW_MIME.contains(mime)) {
            throw new IllegalArgumentException("Unsupported type: " + mime);
        }

        // 3. 扩展名校验
        String ext = FilenameUtils.getExtension(file.getOriginalFilename()).toLowerCase();
        if (!ALLOW_EXT.contains(ext)) {
            throw new IllegalArgumentException("Illegal extension: " + ext);
        }

        // 4. 生成安全文件名:UUID.扩展名
        String safeName = UUID.randomUUID() + "." + ext;

        // 5. 路径规范化并防止穿越
        Path target = baseDir.resolve(safeName).normalize();
        if (!target.startsWith(baseDir)) {
            throw new SecurityException("Path traversal detected");
        }

        // 6. 确保目录存在
        Files.createDirectories(baseDir);

        // 7. 落盘(REPLACE_EXISTING 可覆盖同名 UUID,但 UUID 重复概率≈0)
        Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);

        // 8. 返回可对外暴露的相对路径(如 /2024/07/22/uuid.jpg)
        return baseDir.relativize(target).toString().replace('\\', '/');
    }
}
posted on 2025-07-22 17:04  在努力学习的杰瑞  阅读(246)  评论(0)    收藏  举报