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 629321534..0d8e19bb2 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 @@ -286,5 +286,38 @@ public class SsrfFileTypeFilter { } } + /** + * 校验文件路径安全性,防止路径遍历攻击 + * @param filePath 文件路径 + */ + public static void checkPathTraversal(String filePath) { + if (StringUtils.isBlank(filePath)) { + return; + } + // 1. 防止路径遍历:不允许 .. + if (filePath.contains("..")) { + throw new JeecgBootException("文件路径包含非法字符"); + } + // 2. 防止URL编码绕过:%2e = . + String fileLower = filePath.toLowerCase(); + if (fileLower.contains("%2e")) { + throw new JeecgBootException("文件路径包含非法字符"); + } + } + + /** + * 批量校验文件路径安全性(逗号分隔的多个文件路径) + * @param files 逗号分隔的文件路径 + */ + public static void checkPathTraversalBatch(String files) { + if (StringUtils.isBlank(files)) { + return; + } + for (String file : files.split(",")) { + if (StringUtils.isNotBlank(file)) { + checkPathTraversal(file.trim()); + } + } + } } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-boot-module-airag/src/main/java/org/jeecg/modules/airag/llm/handler/AIChatHandler.java b/jeecg-boot/jeecg-boot-module/jeecg-boot-module-airag/src/main/java/org/jeecg/modules/airag/llm/handler/AIChatHandler.java index f4343570a..569b8d55b 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-boot-module-airag/src/main/java/org/jeecg/modules/airag/llm/handler/AIChatHandler.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-boot-module-airag/src/main/java/org/jeecg/modules/airag/llm/handler/AIChatHandler.java @@ -11,6 +11,7 @@ import lombok.extern.slf4j.Slf4j; import org.jeecg.ai.handler.LLMHandler; import org.jeecg.common.exception.JeecgBootException; import org.jeecg.common.util.AssertUtils; +import org.jeecg.common.util.filter.SsrfFileTypeFilter; import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.airag.common.consts.AiragConsts; import org.jeecg.modules.airag.common.handler.AIChatParams; @@ -401,6 +402,7 @@ public class AIChatHandler implements IAIChatHandler { String filePath = uploadpath + File.separator + imageUrl; // 读取文件并转换为 base64 编码字符串 try { + SsrfFileTypeFilter.checkPathTraversal(filePath); Path path = Paths.get(filePath); byte[] fileContent = Files.readAllBytes(path); String base64Data = Base64.getEncoder().encodeToString(fileContent); @@ -409,7 +411,7 @@ public class AIChatHandler implements IAIChatHandler { // 构建 ImageContent 对象 imageContents.add(ImageContent.from(base64Data, mimeType)); } catch (IOException e) { - log.error("读取文件失败: " + filePath, e); + log.error("读取文件失败: {}", imageUrl, e); throw new RuntimeException("发送消息失败,读取文件异常:" + e.getMessage(), e); } } @@ -529,12 +531,13 @@ public class AIChatHandler implements IAIChatHandler { } else { // 本地文件 String filePath = uploadpath + File.separator + imageUrl; + SsrfFileTypeFilter.checkPathTraversal(filePath); Path path = Paths.get(filePath); fileContent = Files.readAllBytes(path); } originalImageBase64List.add(Base64.getEncoder().encodeToString(fileContent)); } catch (Exception e) { - log.error("图片读取失败: " + imageUrl, e); + log.error("图片读取失败: {}", imageUrl, e); throw new JeecgBootException("图片读取失败: " + imageUrl); } } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysAnnouncementController.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysAnnouncementController.java index f7544d76d..c515ea086 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysAnnouncementController.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysAnnouncementController.java @@ -20,6 +20,7 @@ import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.system.util.JwtUtil; import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.util.*; +import org.jeecg.common.util.filter.SsrfFileTypeFilter; import org.jeecg.config.mybatis.MybatisPlusSaasConfig; import org.jeecg.modules.message.enums.RangeDateEnum; import org.jeecg.modules.message.websocket.WebSocket; @@ -142,6 +143,8 @@ public class SysAnnouncementController { // 代码逻辑说明: 标题处理xss攻击的问题 String title = XssUtils.scriptXss(sysAnnouncement.getTitile()); sysAnnouncement.setTitile(title); + // 【安全校验】校验附件文件名,防止路径遍历攻击 + SsrfFileTypeFilter.checkPathTraversalBatch(sysAnnouncement.getFiles()); sysAnnouncement.setDelFlag(CommonConstant.DEL_FLAG_0.toString()); //未发布 sysAnnouncement.setSendStatus(CommonSendStatus.UNPUBLISHED_STATUS_0); @@ -173,6 +176,8 @@ public class SysAnnouncementController { // 代码逻辑说明: 标题处理xss攻击的问题 String title = XssUtils.scriptXss(sysAnnouncement.getTitile()); sysAnnouncement.setTitile(title); + // 【安全校验】校验附件文件名,防止路径遍历攻击 + SsrfFileTypeFilter.checkPathTraversalBatch(sysAnnouncement.getFiles()); sysAnnouncement.setNoticeType(NoticeTypeEnum.NOTICE_TYPE_SYSTEM.getValue()); boolean ok = sysAnnouncementService.upDateAnnouncement(sysAnnouncement); //TODO 返回false说明什么? diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysAnnouncementServiceImpl.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysAnnouncementServiceImpl.java index be85d30f8..066391a6d 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysAnnouncementServiceImpl.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysAnnouncementServiceImpl.java @@ -12,6 +12,7 @@ import org.apache.shiro.SecurityUtils; import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.util.FileDownloadUtils; +import org.jeecg.common.util.filter.SsrfFileTypeFilter; import org.jeecg.common.util.oConvertUtils; import org.jeecg.config.JeecgBaseConfig; import org.jeecg.config.mybatis.MybatisPlusSaasConfig; @@ -303,6 +304,8 @@ public class SysAnnouncementServiceImpl extends ServiceImpl