diff --git a/server/src/main/java/cn/keking/config/WebConfig.java b/server/src/main/java/cn/keking/config/WebConfig.java index b1ee63c0..c42e9e73 100644 --- a/server/src/main/java/cn/keking/config/WebConfig.java +++ b/server/src/main/java/cn/keking/config/WebConfig.java @@ -46,6 +46,7 @@ public class WebConfig implements WebMvcConfigurer { filterUri.add("/onlinePreview"); filterUri.add("/picturesPreview"); filterUri.add("/getCorsFile"); + filterUri.add("/addTask"); filterUri.add("/pdfjs/web/viewer.html"); filterUri.add("/msg/index.html"); filterUri.add("/eml/index.html"); @@ -64,6 +65,7 @@ public class WebConfig implements WebMvcConfigurer { filterUri.add("/onlinePreview"); filterUri.add("/picturesPreview"); filterUri.add("/getCorsFile"); + filterUri.add("/addTask"); TrustDirFilter filter = new TrustDirFilter(); FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(filter); diff --git a/server/src/main/java/cn/keking/service/FileConvertQueueTask.java b/server/src/main/java/cn/keking/service/FileConvertQueueTask.java index 6096b92b..c4e5e9fa 100644 --- a/server/src/main/java/cn/keking/service/FileConvertQueueTask.java +++ b/server/src/main/java/cn/keking/service/FileConvertQueueTask.java @@ -3,6 +3,8 @@ package cn.keking.service; import cn.keking.model.FileAttribute; import cn.keking.model.FileType; import cn.keking.service.cache.CacheService; +import cn.keking.web.filter.TrustDirFilter; +import cn.keking.web.filter.TrustHostFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -58,6 +60,10 @@ public class FileConvertQueueTask { try { url = cacheService.takeQueueTask(); if (url != null) { + if (!TrustHostFilter.isTrustedSourceUrl(url) || !TrustDirFilter.isTrustedFileUrl(url)) { + logger.warn("拒绝处理不受信任的预览转换任务,url:{}", url); + continue; + } FileAttribute fileAttribute = fileHandlerService.getFileAttribute(url, null); FileType fileType = fileAttribute.getType(); logger.info("正在处理预览转换任务,url:{},预览类型:{}", url, fileType); diff --git a/server/src/main/java/cn/keking/web/controller/FileController.java b/server/src/main/java/cn/keking/web/controller/FileController.java index 247b6204..e20633fb 100644 --- a/server/src/main/java/cn/keking/web/controller/FileController.java +++ b/server/src/main/java/cn/keking/web/controller/FileController.java @@ -29,6 +29,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.DirectoryStream; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; @@ -147,6 +148,28 @@ public class FileController { } } + private Path getDemoBasePath() { + return Paths.get(fileDir, demoDir).toAbsolutePath().normalize(); + } + + private Path resolveDemoPath(String path) { + Path demoBasePath = getDemoBasePath(); + try { + if (ObjectUtils.isEmpty(path)) { + return demoBasePath; + } + Path normalizedPath = demoBasePath.resolve(path).normalize(); + if (!normalizedPath.startsWith(demoBasePath)) { + logger.warn("检测到非法目录访问,path:{}", path); + return null; + } + return normalizedPath; + } catch (InvalidPathException e) { + logger.warn("解析目录路径失败,path:{}", path, e); + return null; + } + } + @PostMapping("/fileUpload") public ReturnResponse fileUpload(@RequestParam("file") MultipartFile file, @RequestParam(value = "path", defaultValue = "") String path) { @@ -341,17 +364,20 @@ public class FileController { } // ==================== 2. 构建路径和验证 ==================== - String basePath = fileDir + demoPath; - if (!ObjectUtils.isEmpty(path)) { - basePath += path + File.separator; + Path resolvedPath = resolveDemoPath(path); + if (resolvedPath == null) { + result.put("total", 0); + result.put("data", Collections.emptyList()); + return result; } - File currentDir = new File(basePath); + File currentDir = resolvedPath.toFile(); if (!currentDir.exists() || !currentDir.isDirectory()) { result.put("total", 0); result.put("data", Collections.emptyList()); return result; } + String basePath = resolvedPath.toString(); // ==================== 3. 收集所有文件路径 ==================== List allPaths = new ArrayList<>(); diff --git a/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java b/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java index d772ced1..2110685d 100644 --- a/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java +++ b/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java @@ -8,6 +8,8 @@ import cn.keking.service.FilePreviewFactory; import cn.keking.service.cache.CacheService; import cn.keking.service.impl.OtherFilePreviewImpl; import cn.keking.utils.*; +import cn.keking.web.filter.TrustDirFilter; +import cn.keking.web.filter.TrustHostFilter; import fr.opensagres.xdocreport.core.io.IOUtils; import org.apache.commons.codec.binary.Base64; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; @@ -231,6 +233,11 @@ public class OnlinePreviewController { logger.info("{},url:{}", errorMsg, fileUrls); return errorMsg; } + if (!TrustHostFilter.isTrustedSourceUrl(fileUrls) || !TrustDirFilter.isTrustedFileUrl(fileUrls)) { + String errorMsg = "访问不合法:来源地址不受信任!"; + logger.info("{},url:{}", errorMsg, fileUrls); + return errorMsg; + } logger.info("添加转码队列url:{}", fileUrls); cacheService.addQueueTask(fileUrls); return "success"; diff --git a/server/src/main/java/cn/keking/web/filter/TrustDirFilter.java b/server/src/main/java/cn/keking/web/filter/TrustDirFilter.java index f71e9fd3..896f8f2e 100644 --- a/server/src/main/java/cn/keking/web/filter/TrustDirFilter.java +++ b/server/src/main/java/cn/keking/web/filter/TrustDirFilter.java @@ -28,7 +28,7 @@ import java.util.Locale; public class TrustDirFilter implements Filter { private String notTrustDirView; - private final Logger logger = LoggerFactory.getLogger(TrustDirFilter.class); + private static final Logger logger = LoggerFactory.getLogger(TrustDirFilter.class); @Override @@ -59,6 +59,47 @@ public class TrustDirFilter implements Filter { } + public static boolean isTrustedFileUrl(String urlPath) { + // 判断URL是否合法 + if (KkFileUtils.isIllegalFileName(urlPath) || !StringUtils.hasText(urlPath) || !WebUtils.isValidUrl(urlPath)) { + return false; + } + try { + URL url = WebUtils.normalizedURL(urlPath); + + if ("file".equals(url.getProtocol().toLowerCase(Locale.ROOT))) { + String filePath = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8.name()); + // 将文件路径转换为File对象 + File targetFile = new File(filePath); + // 将配置目录也转换为File对象 + File fileDir = new File(ConfigConstants.getFileDir()); + File localPreviewDir = new File(ConfigConstants.getLocalPreviewDir()); + try { + // 获取规范路径 + String canonicalFilePath = targetFile.getCanonicalPath(); + String canonicalFileDir = fileDir.getCanonicalPath(); + String canonicalLocalPreviewDir = localPreviewDir.getCanonicalPath(); + return isSubDirectory(canonicalFileDir, canonicalFilePath) || isSubDirectory(canonicalLocalPreviewDir, canonicalFilePath); + } catch (IOException e) { + LoggerFactory.getLogger(TrustDirFilter.class).warn("获取规范路径失败,使用原始路径比较", e); + String absFilePath = targetFile.getAbsolutePath(); + String absFileDir = fileDir.getAbsolutePath(); + String absLocalPreviewDir = localPreviewDir.getAbsolutePath(); + absFilePath = absFilePath.replace('\\', '/'); + absFileDir = absFileDir.replace('\\', '/'); + absLocalPreviewDir = absLocalPreviewDir.replace('\\', '/'); + if (!absFileDir.endsWith("/")) absFileDir += "/"; + if (!absLocalPreviewDir.endsWith("/")) absLocalPreviewDir += "/"; + return absFilePath.startsWith(absFileDir) || absFilePath.startsWith(absLocalPreviewDir); + } + } + return true; + } catch (IOException | GalimatiasParseException e) { + LoggerFactory.getLogger(TrustDirFilter.class).error("解析URL异常,url:{}", urlPath, e); + return false; + } + } + private boolean allowPreview(String urlPath) { // 判断URL是否合法 if (KkFileUtils.isIllegalFileName(urlPath) || !StringUtils.hasText(urlPath) || !WebUtils.isValidUrl(urlPath)) { @@ -107,7 +148,7 @@ public class TrustDirFilter implements Filter { /** * 检查子路径是否在父路径下(跨平台) */ - private boolean isSubDirectory(String parentDir, String childPath) { + private static boolean isSubDirectory(String parentDir, String childPath) { try { File parent = new File(parentDir); File child = new File(childPath); diff --git a/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java b/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java index 759a573a..9a4fb577 100644 --- a/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java +++ b/server/src/main/java/cn/keking/web/filter/TrustHostFilter.java @@ -70,6 +70,14 @@ public class TrustHostFilter implements Filter { } } + public static boolean isTrustedSourceUrl(String url) { + if (!WebUtils.isValidUrl(url)) { + return false; + } + String host = WebUtils.getHost(url); + return !new TrustHostFilter().isNotTrustHost(host); + } + public boolean isNotTrustHost(String host) { if (host == null || host.trim().isEmpty()) { logger.warn("主机名为空或无效,拒绝访问");