fix: harden listFiles and addTask validation

This commit is contained in:
kl
2026-06-11 10:16:34 +08:00
parent 1568b3023d
commit c6df85be1b
6 changed files with 96 additions and 6 deletions

View File

@@ -46,6 +46,7 @@ public class WebConfig implements WebMvcConfigurer {
filterUri.add("/onlinePreview"); filterUri.add("/onlinePreview");
filterUri.add("/picturesPreview"); filterUri.add("/picturesPreview");
filterUri.add("/getCorsFile"); filterUri.add("/getCorsFile");
filterUri.add("/addTask");
filterUri.add("/pdfjs/web/viewer.html"); filterUri.add("/pdfjs/web/viewer.html");
filterUri.add("/msg/index.html"); filterUri.add("/msg/index.html");
filterUri.add("/eml/index.html"); filterUri.add("/eml/index.html");
@@ -64,6 +65,7 @@ public class WebConfig implements WebMvcConfigurer {
filterUri.add("/onlinePreview"); filterUri.add("/onlinePreview");
filterUri.add("/picturesPreview"); filterUri.add("/picturesPreview");
filterUri.add("/getCorsFile"); filterUri.add("/getCorsFile");
filterUri.add("/addTask");
TrustDirFilter filter = new TrustDirFilter(); TrustDirFilter filter = new TrustDirFilter();
FilterRegistrationBean<TrustDirFilter> registrationBean = new FilterRegistrationBean<>(); FilterRegistrationBean<TrustDirFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(filter); registrationBean.setFilter(filter);

View File

@@ -3,6 +3,8 @@ package cn.keking.service;
import cn.keking.model.FileAttribute; import cn.keking.model.FileAttribute;
import cn.keking.model.FileType; import cn.keking.model.FileType;
import cn.keking.service.cache.CacheService; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -58,6 +60,10 @@ public class FileConvertQueueTask {
try { try {
url = cacheService.takeQueueTask(); url = cacheService.takeQueueTask();
if (url != null) { if (url != null) {
if (!TrustHostFilter.isTrustedSourceUrl(url) || !TrustDirFilter.isTrustedFileUrl(url)) {
logger.warn("拒绝处理不受信任的预览转换任务url{}", url);
continue;
}
FileAttribute fileAttribute = fileHandlerService.getFileAttribute(url, null); FileAttribute fileAttribute = fileHandlerService.getFileAttribute(url, null);
FileType fileType = fileAttribute.getType(); FileType fileType = fileAttribute.getType();
logger.info("正在处理预览转换任务url{},预览类型:{}", url, fileType); logger.info("正在处理预览转换任务url{},预览类型:{}", url, fileType);

View File

@@ -29,6 +29,7 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes; 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") @PostMapping("/fileUpload")
public ReturnResponse<Object> fileUpload(@RequestParam("file") MultipartFile file, public ReturnResponse<Object> fileUpload(@RequestParam("file") MultipartFile file,
@RequestParam(value = "path", defaultValue = "") String path) { @RequestParam(value = "path", defaultValue = "") String path) {
@@ -341,17 +364,20 @@ public class FileController {
} }
// ==================== 2. 构建路径和验证 ==================== // ==================== 2. 构建路径和验证 ====================
String basePath = fileDir + demoPath; Path resolvedPath = resolveDemoPath(path);
if (!ObjectUtils.isEmpty(path)) { if (resolvedPath == null) {
basePath += path + File.separator; 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()) { if (!currentDir.exists() || !currentDir.isDirectory()) {
result.put("total", 0); result.put("total", 0);
result.put("data", Collections.emptyList()); result.put("data", Collections.emptyList());
return result; return result;
} }
String basePath = resolvedPath.toString();
// ==================== 3. 收集所有文件路径 ==================== // ==================== 3. 收集所有文件路径 ====================
List<Path> allPaths = new ArrayList<>(); List<Path> allPaths = new ArrayList<>();

View File

@@ -8,6 +8,8 @@ import cn.keking.service.FilePreviewFactory;
import cn.keking.service.cache.CacheService; import cn.keking.service.cache.CacheService;
import cn.keking.service.impl.OtherFilePreviewImpl; import cn.keking.service.impl.OtherFilePreviewImpl;
import cn.keking.utils.*; import cn.keking.utils.*;
import cn.keking.web.filter.TrustDirFilter;
import cn.keking.web.filter.TrustHostFilter;
import fr.opensagres.xdocreport.core.io.IOUtils; import fr.opensagres.xdocreport.core.io.IOUtils;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
@@ -231,6 +233,11 @@ public class OnlinePreviewController {
logger.info("{}url{}", errorMsg, fileUrls); logger.info("{}url{}", errorMsg, fileUrls);
return errorMsg; return errorMsg;
} }
if (!TrustHostFilter.isTrustedSourceUrl(fileUrls) || !TrustDirFilter.isTrustedFileUrl(fileUrls)) {
String errorMsg = "访问不合法:来源地址不受信任!";
logger.info("{}url{}", errorMsg, fileUrls);
return errorMsg;
}
logger.info("添加转码队列url{}", fileUrls); logger.info("添加转码队列url{}", fileUrls);
cacheService.addQueueTask(fileUrls); cacheService.addQueueTask(fileUrls);
return "success"; return "success";

View File

@@ -28,7 +28,7 @@ import java.util.Locale;
public class TrustDirFilter implements Filter { public class TrustDirFilter implements Filter {
private String notTrustDirView; private String notTrustDirView;
private final Logger logger = LoggerFactory.getLogger(TrustDirFilter.class); private static final Logger logger = LoggerFactory.getLogger(TrustDirFilter.class);
@Override @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) { private boolean allowPreview(String urlPath) {
// 判断URL是否合法 // 判断URL是否合法
if (KkFileUtils.isIllegalFileName(urlPath) || !StringUtils.hasText(urlPath) || !WebUtils.isValidUrl(urlPath)) { 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 { try {
File parent = new File(parentDir); File parent = new File(parentDir);
File child = new File(childPath); File child = new File(childPath);

View File

@@ -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) { public boolean isNotTrustHost(String host) {
if (host == null || host.trim().isEmpty()) { if (host == null || host.trim().isEmpty()) {
logger.warn("主机名为空或无效,拒绝访问"); logger.warn("主机名为空或无效,拒绝访问");