mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-09-13 00:14:56 +00:00
Merge commit from fork
This commit is contained in:
@@ -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;
|
||||||
@@ -341,13 +342,23 @@ public class FileController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 2. 构建路径和验证 ====================
|
// ==================== 2. 构建路径和验证 ====================
|
||||||
String basePath = fileDir + demoPath;
|
Path currentDir;
|
||||||
if (!ObjectUtils.isEmpty(path)) {
|
try {
|
||||||
basePath += path + File.separator;
|
currentDir = resolveDirectoryUnderRoot(Paths.get(fileDir, demoDir), path);
|
||||||
|
} catch (InvalidPathException | SecurityException e) {
|
||||||
|
logger.warn("拒绝访问 demo 目录之外的文件列表路径");
|
||||||
|
result.put("total", 0);
|
||||||
|
result.put("data", Collections.emptyList());
|
||||||
|
result.put("error", "非法目录路径");
|
||||||
|
return result;
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("解析 demo 目录失败", e);
|
||||||
|
result.put("total", 0);
|
||||||
|
result.put("data", Collections.emptyList());
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
File currentDir = new File(basePath);
|
if (!Files.isDirectory(currentDir)) {
|
||||||
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;
|
||||||
@@ -357,13 +368,13 @@ public class FileController {
|
|||||||
List<Path> allPaths = new ArrayList<>();
|
List<Path> allPaths = new ArrayList<>();
|
||||||
long collectStartTime = System.currentTimeMillis();
|
long collectStartTime = System.currentTimeMillis();
|
||||||
|
|
||||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(basePath))) {
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(currentDir)) {
|
||||||
for (Path entry : stream) {
|
for (Path entry : stream) {
|
||||||
allPaths.add(entry);
|
allPaths.add(entry);
|
||||||
stats.incrementFileCount();
|
stats.incrementFileCount();
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("读取目录失败: {}", basePath, e);
|
logger.error("读取目录失败: {}", currentDir, e);
|
||||||
result.put("total", 0);
|
result.put("total", 0);
|
||||||
result.put("data", Collections.emptyList());
|
result.put("data", Collections.emptyList());
|
||||||
return result;
|
return result;
|
||||||
@@ -492,6 +503,46 @@ public class FileController {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve an existing directory below the configured demo root.
|
||||||
|
*
|
||||||
|
* <p>Both lexical normalization and real-path checks are required: the
|
||||||
|
* former blocks traversal and absolute paths, while the latter prevents a
|
||||||
|
* symlink inside the demo directory from escaping the configured root.</p>
|
||||||
|
*/
|
||||||
|
static Path resolveDirectoryUnderRoot(Path root, String requestedPath) throws IOException {
|
||||||
|
Path normalizedRoot = root.toAbsolutePath().normalize();
|
||||||
|
String relativePath = requestedPath == null ? "" : requestedPath.replace('\\', '/');
|
||||||
|
|
||||||
|
if (relativePath.indexOf('\0') >= 0
|
||||||
|
|| relativePath.startsWith("/")
|
||||||
|
|| relativePath.matches("^[A-Za-z]:.*")) {
|
||||||
|
throw new SecurityException("Absolute paths are not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
Path relative = Paths.get(relativePath);
|
||||||
|
if (relative.isAbsolute()) {
|
||||||
|
throw new SecurityException("Absolute paths are not allowed");
|
||||||
|
}
|
||||||
|
for (Path segment : relative) {
|
||||||
|
if ("..".equals(segment.toString())) {
|
||||||
|
throw new SecurityException("Parent path segments are not allowed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Path resolved = normalizedRoot.resolve(relative).normalize();
|
||||||
|
if (!resolved.startsWith(normalizedRoot)) {
|
||||||
|
throw new SecurityException("Path escapes the configured root");
|
||||||
|
}
|
||||||
|
|
||||||
|
Path realRoot = normalizedRoot.toRealPath();
|
||||||
|
Path realResolved = resolved.toRealPath();
|
||||||
|
if (!realResolved.startsWith(realRoot)) {
|
||||||
|
throw new SecurityException("Path escapes the configured root through a symbolic link");
|
||||||
|
}
|
||||||
|
return realResolved;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建性能统计信息
|
* 构建性能统计信息
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package cn.keking.web.controller;
|
||||||
|
|
||||||
|
import cn.keking.config.ConfigConstants;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Assumptions;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class FileControllerPathSecurityTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
private String originalFileDir;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void rememberConfiguredFileDirectory() {
|
||||||
|
originalFileDir = ConfigConstants.getFileDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void restoreConfiguredFileDirectory() {
|
||||||
|
ConfigConstants.setFileDirValue(originalFileDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveDirectoriesInsideDemoRoot() throws IOException {
|
||||||
|
Path demoRoot = Files.createDirectory(tempDir.resolve("demo"));
|
||||||
|
Path nested = Files.createDirectories(demoRoot.resolve("folder/subfolder"));
|
||||||
|
|
||||||
|
assertEquals(demoRoot.toRealPath(), FileController.resolveDirectoryUnderRoot(demoRoot, ""));
|
||||||
|
assertEquals(nested.toRealPath(), FileController.resolveDirectoryUnderRoot(demoRoot, "folder/subfolder"));
|
||||||
|
assertEquals(nested.toRealPath(), FileController.resolveDirectoryUnderRoot(demoRoot, "folder\\subfolder"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectParentTraversalWithEitherSeparator() throws IOException {
|
||||||
|
Path demoRoot = Files.createDirectory(tempDir.resolve("demo"));
|
||||||
|
|
||||||
|
assertThrows(SecurityException.class,
|
||||||
|
() -> FileController.resolveDirectoryUnderRoot(demoRoot, "../outside"));
|
||||||
|
assertThrows(SecurityException.class,
|
||||||
|
() -> FileController.resolveDirectoryUnderRoot(demoRoot, "..\\outside"));
|
||||||
|
assertThrows(SecurityException.class,
|
||||||
|
() -> FileController.resolveDirectoryUnderRoot(demoRoot, "folder/../outside"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectAbsoluteDriveAndUncPaths() throws IOException {
|
||||||
|
Path demoRoot = Files.createDirectory(tempDir.resolve("demo"));
|
||||||
|
|
||||||
|
assertThrows(SecurityException.class,
|
||||||
|
() -> FileController.resolveDirectoryUnderRoot(demoRoot, "/etc"));
|
||||||
|
assertThrows(SecurityException.class,
|
||||||
|
() -> FileController.resolveDirectoryUnderRoot(demoRoot, "C:\\Windows"));
|
||||||
|
assertThrows(SecurityException.class,
|
||||||
|
() -> FileController.resolveDirectoryUnderRoot(demoRoot, "\\\\server\\share"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectSymlinkThatEscapesDemoRoot() throws IOException {
|
||||||
|
Path demoRoot = Files.createDirectory(tempDir.resolve("demo"));
|
||||||
|
Path outside = Files.createDirectory(tempDir.resolve("outside"));
|
||||||
|
Path link = demoRoot.resolve("outside-link");
|
||||||
|
try {
|
||||||
|
Files.createSymbolicLink(link, outside);
|
||||||
|
} catch (IOException | UnsupportedOperationException e) {
|
||||||
|
Assumptions.assumeTrue(false, "Symbolic links are unavailable in this environment");
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThrows(SecurityException.class,
|
||||||
|
() -> FileController.resolveDirectoryUnderRoot(demoRoot, "outside-link"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listFilesShouldNotExposeEntriesOutsideDemoRoot() throws IOException {
|
||||||
|
Files.createDirectory(tempDir.resolve("demo"));
|
||||||
|
Files.createFile(tempDir.resolve("outside-secret.txt"));
|
||||||
|
ConfigConstants.setFileDirValue(tempDir.toString());
|
||||||
|
FileController controller = new FileController();
|
||||||
|
|
||||||
|
Map<String, Object> result = controller.getFiles("..", "", 0, 20, null, null);
|
||||||
|
|
||||||
|
assertEquals("非法目录路径", result.get("error"));
|
||||||
|
assertTrue(((List<?>) result.get("data")).isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user