mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-06-15 10:27:12 +00:00
fix: harden listFiles and addTask validation
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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<>();
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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("主机名为空或无效,拒绝访问");
|
||||||
|
|||||||
Reference in New Issue
Block a user