From d3b8948f402d51f03a04a2d6c7f04df7621e64c8 Mon Sep 17 00:00:00 2001 From: JEECG <445654970@qq.com> Date: Mon, 29 Sep 2025 18:29:11 +0800 Subject: [PATCH] Path Traversal Vulnerability /sys/comment/addFile /sys/upload/uploadMinio endpoint (notice the uploadlocal function is different from the /sys/common/upload ) #8827 --- .../org/jeecg/common/util/CommonUtils.java | 6 +- .../java/org/jeecg/common/util/MinioUtil.java | 8 +-- .../util/filter/SsrfFileTypeFilter.java | 61 +++++++++++++++---- .../jeecg/common/util/oss/OssBootUtil.java | 3 +- .../system/controller/CommonController.java | 43 ++----------- .../controller/SysUploadController.java | 18 +++--- .../service/impl/SysCommentServiceImpl.java | 36 +++++------ 7 files changed, 89 insertions(+), 86 deletions(-) diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/CommonUtils.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/CommonUtils.java index c4ca4e92b..2da751562 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/CommonUtils.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/CommonUtils.java @@ -153,9 +153,9 @@ public class CommonUtils { */ public static String uploadLocal(MultipartFile mf,String bizPath,String uploadpath){ try { - //update-begin-author:liusq date:20210809 for: 过滤上传文件类型 - SsrfFileTypeFilter.checkUploadFileType(mf); - //update-end-author:liusq date:20210809 for: 过滤上传文件类型 + // 文件安全校验,防止上传漏洞文件 + SsrfFileTypeFilter.checkUploadFileType(mf, bizPath); + String fileName = null; File file = new File(uploadpath + File.separator + bizPath + File.separator ); if (!file.exists()) { diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/MinioUtil.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/MinioUtil.java index d5a13c0bc..150b407b7 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/MinioUtil.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/MinioUtil.java @@ -55,13 +55,11 @@ public class MinioUtil { */ public static String upload(MultipartFile file, String bizPath, String customBucket) throws Exception { String fileUrl = ""; - //update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击 + // 业务路径过滤,防止攻击 bizPath = StrAttackFilter.filter(bizPath); - //update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击 - //update-begin-author:liusq date:20210809 for: 过滤上传文件类型 - SsrfFileTypeFilter.checkUploadFileType(file); - //update-end-author:liusq date:20210809 for: 过滤上传文件类型 + // 文件安全校验,防止上传漏洞文件 + SsrfFileTypeFilter.checkUploadFileType(file, bizPath); String newBucket = bucketName; if(oConvertUtils.isNotEmpty(customBucket)){ diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/filter/SsrfFileTypeFilter.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/filter/SsrfFileTypeFilter.java index 6376dff95..e72613794 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/filter/SsrfFileTypeFilter.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/filter/SsrfFileTypeFilter.java @@ -2,6 +2,7 @@ package org.jeecg.common.util.filter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.jeecg.common.exception.JeecgBootException; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; @@ -149,29 +150,38 @@ public class SsrfFileTypeFilter { public static void checkDownloadFileType(String filePath) throws IOException { //文件后缀 String suffix = getFileTypeBySuffix(filePath); - log.info("suffix:{}", suffix); + log.debug(" 【文件下载校验】文件后缀 suffix: {}", suffix); boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase()); //是否允许下载的文件 if (!isAllowExtension) { - throw new IOException("下载失败,存在非法文件类型:" + suffix); + throw new JeecgBootException("下载失败,存在非法文件类型:" + suffix); } } - /** * 上传文件类型过滤 * * @param file */ public static void checkUploadFileType(MultipartFile file) throws Exception { - //获取文件真是后缀 - String suffix = getFileType(file); - - log.info("suffix:{}", suffix); + checkUploadFileType(file, null); + } + + /** + * 上传文件类型过滤 + * + * @param file + */ + public static void checkUploadFileType(MultipartFile file, String customPath) throws Exception { + //1. 路径安全校验 + validatePathSecurity(customPath); + //2. 校验文件后缀和头 + String suffix = getFileType(file, customPath); + log.info("【文件上传校验】文件后缀 suffix: {},customPath:{}", suffix, customPath); boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase()); //是否允许下载的文件 if (!isAllowExtension) { - throw new Exception("上传失败,存在非法文件类型:" + suffix); + throw new JeecgBootException("上传失败,存在非法文件类型:" + suffix); } } @@ -183,7 +193,7 @@ public class SsrfFileTypeFilter { * @throws Exception */ - private static String getFileType(MultipartFile file) throws Exception { + private static String getFileType(MultipartFile file, String customPath) throws Exception { //update-begin-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件 String fileExtendName = null; InputStream is = null; @@ -203,7 +213,7 @@ public class SsrfFileTypeFilter { break; } } - log.info("-----获取到的指定文件类型------"+fileExtendName); + log.debug("-----获取到的指定文件类型------"+fileExtendName); // 如果不是上述类型,则判断扩展名 if (StringUtils.isBlank(fileExtendName)) { String fileName = file.getOriginalFilename(); @@ -214,7 +224,6 @@ public class SsrfFileTypeFilter { // 如果有扩展名,则返回扩展名 return getFileTypeBySuffix(fileName); } - log.info("-----最終的文件类型------"+fileExtendName); is.close(); return fileExtendName; } catch (Exception e) { @@ -249,4 +258,34 @@ public class SsrfFileTypeFilter { } return stringBuilder.toString(); } + + /** + * 路径安全校验 + */ + private static void validatePathSecurity(String customPath) throws JeecgBootException { + if (customPath == null || customPath.trim().isEmpty()) { + return; + } + + // 统一分隔符为 / + String normalized = customPath.replace("\\", "/"); + + // 1. 防止路径遍历攻击 + if (normalized.contains("..") || normalized.contains("~")) { + throw new JeecgBootException("上传业务路径包含非法字符!"); + } + + // 2. 限制路径深度 + int depth = normalized.split("/").length; + if (depth > 5) { + throw new JeecgBootException("上传业务路径深度超出限制!"); + } + + // 3. 限制字符集(只允许字母、数字、下划线、横线、斜杠) + if (!normalized.matches("^[a-zA-Z0-9/_-]+$")) { + throw new JeecgBootException("上传业务路径包含非法字符!"); + } + } + + } diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/oss/OssBootUtil.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/oss/OssBootUtil.java index bafc035ff..e08001800 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/oss/OssBootUtil.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/oss/OssBootUtil.java @@ -97,9 +97,8 @@ public class OssBootUtil { * @return oss 中的相对文件路径 */ public static String upload(MultipartFile file, String fileDir,String customBucket) throws Exception { - //update-begin-author:liusq date:20210809 for: 过滤上传文件类型 + // 文件安全校验,防止上传漏洞文件 SsrfFileTypeFilter.checkUploadFileType(file); - //update-end-author:liusq date:20210809 for: 过滤上传文件类型 String filePath = null; initOss(endPoint, accessKeyId, accessKeySecret); diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/CommonController.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/CommonController.java index 4429eb66f..27f7c6d8e 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/CommonController.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/CommonController.java @@ -68,50 +68,19 @@ public class CommonController { Result result = new Result<>(); String savePath = ""; String bizPath = request.getParameter("biz"); - - //LOWCOD-2580 sys/common/upload接口存在任意文件上传漏洞 - if (oConvertUtils.isNotEmpty(bizPath)) { - if(bizPath.contains(SymbolConstant.SPOT_SINGLE_SLASH) || bizPath.contains(SymbolConstant.SPOT_DOUBLE_BACKSLASH)){ - throw new JeecgBootException("上传目录bizPath,格式非法!"); - } - } - MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; - // 获取上传文件对象 MultipartFile file = multipartRequest.getFile("file"); - if(oConvertUtils.isEmpty(bizPath)){ - if(CommonConstant.UPLOAD_TYPE_OSS.equals(uploadType)){ - //未指定目录,则用阿里云默认目录 upload - bizPath = "upload"; - //result.setMessage("使用阿里云文件上传时,必须添加目录!"); - //result.setSuccess(false); - //return result; - }else{ - bizPath = ""; - } + + // 文件安全校验,防止上传漏洞文件 + SsrfFileTypeFilter.checkUploadFileType(file, bizPath); + + if (oConvertUtils.isEmpty(bizPath)) { + bizPath = CommonConstant.UPLOAD_TYPE_OSS.equals(uploadType) ? "upload" : ""; } if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){ - //update-begin-author:liusq date:20221102 for: 过滤上传文件类型 - SsrfFileTypeFilter.checkUploadFileType(file); - //update-end-author:liusq date:20221102 for: 过滤上传文件类型 - //update-begin-author:lvdandan date:20200928 for:修改JEditor编辑器本地上传 savePath = this.uploadLocal(file,bizPath); - //update-begin-author:lvdandan date:20200928 for:修改JEditor编辑器本地上传 - /** 富文本编辑器及markdown本地上传时,采用返回链接方式 - //针对jeditor编辑器如何使 lcaol模式,采用 base64格式存储 - String jeditor = request.getParameter("jeditor"); - if(oConvertUtils.isNotEmpty(jeditor)){ - result.setMessage(CommonConstant.UPLOAD_TYPE_LOCAL); - result.setSuccess(true); - return result; - }else{ - savePath = this.uploadLocal(file,bizPath); - } - */ }else{ - //update-begin-author:taoyan date:20200814 for:文件上传改造 savePath = CommonUtils.upload(file, bizPath, uploadType); - //update-end-author:taoyan date:20200814 for:文件上传改造 } if(oConvertUtils.isNotEmpty(savePath)){ result.setMessage(savePath); diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUploadController.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUploadController.java index a6e253506..80652bd97 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUploadController.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUploadController.java @@ -2,9 +2,9 @@ package org.jeecg.modules.system.controller; import lombok.extern.slf4j.Slf4j; import org.jeecg.common.api.vo.Result; -import org.jeecg.common.exception.JeecgBootException; import org.jeecg.common.util.CommonUtils; import org.jeecg.common.util.MinioUtil; +import org.jeecg.common.util.filter.SsrfFileTypeFilter; import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.oss.entity.OssFile; import org.jeecg.modules.oss.service.IOssFileService; @@ -35,20 +35,18 @@ public class SysUploadController { @PostMapping(value = "/uploadMinio") public Result uploadMinio(HttpServletRequest request) throws Exception { Result result = new Result<>(); + // 获取业务路径 String bizPath = request.getParameter("biz"); - - //LOWCOD-2580 sys/common/upload接口存在任意文件上传漏洞 - boolean flag = oConvertUtils.isNotEmpty(bizPath) && (bizPath.contains("../") || bizPath.contains("..\\")); - if (flag) { - throw new JeecgBootException("上传目录bizPath,格式非法!"); - } + // 获取上传文件对象 + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + MultipartFile file = multipartRequest.getFile("file"); + + // 文件安全校验,防止上传漏洞文件 + SsrfFileTypeFilter.checkUploadFileType(file, bizPath); if(oConvertUtils.isEmpty(bizPath)){ bizPath = ""; } - MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; - // 获取上传文件对象 - MultipartFile file = multipartRequest.getFile("file"); // 获取文件名 String orgName = file.getOriginalFilename(); orgName = CommonUtils.getFileName(orgName); diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysCommentServiceImpl.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysCommentServiceImpl.java index 31dfa83ae..90f8eb327 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysCommentServiceImpl.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysCommentServiceImpl.java @@ -15,6 +15,7 @@ import org.jeecg.common.system.api.ISysBaseAPI; import org.jeecg.common.system.vo.SysFilesModel; import org.jeecg.common.util.CommonUtils; import org.jeecg.common.util.RedisUtil; +import org.jeecg.common.util.filter.SsrfFileTypeFilter; import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.system.entity.SysComment; import org.jeecg.modules.system.entity.SysFormFile; @@ -119,28 +120,24 @@ public class SysCommentServiceImpl extends ServiceImpl