task-151: 路径安全校验(PathSafetyGuard 规范化 startsWith 判定、../与绝对路径与符号链接逃逸拒绝、接入清理路径)+ 8 条测试
This commit is contained in:
+97
@@ -0,0 +1,97 @@
|
||||
package com.nanri.aiimage.modules.file.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* task-151:路径安全校验契约(plan 09)。
|
||||
* 删除前校验:child 规范化后 startsWith(临时根目录) 且不等于根;
|
||||
* 穿越(../、绝对路径逃逸、符号链接逃逸)拒绝;非法输入安全返回 false。
|
||||
*/
|
||||
class PathSafetyGuardTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void insideRootIsAllowed() throws Exception {
|
||||
Path child = tempDir.resolve("a.tmp");
|
||||
Files.writeString(child, "x");
|
||||
|
||||
assertTrue(PathSafetyGuard.isInside(tempDir.toFile(), child.toFile()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedDirectoryInsideRootIsAllowed() throws Exception {
|
||||
Path nested = tempDir.resolve("result/2026/09").resolve("f.tmp");
|
||||
Files.createDirectories(nested.getParent());
|
||||
Files.writeString(nested, "x");
|
||||
|
||||
assertTrue(PathSafetyGuard.isInside(tempDir.toFile(), nested.toFile()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parentTraversalIsRejected() {
|
||||
assertTrue(PathSafetyGuard.isTraversal("../escape"));
|
||||
assertTrue(PathSafetyGuard.isTraversal("a/../../b"));
|
||||
assertFalse(PathSafetyGuard.isTraversal("normal-file.tmp"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void absolutePathEscapeIsRejected() {
|
||||
assertTrue(PathSafetyGuard.isTraversal("/etc/passwd"));
|
||||
assertTrue(PathSafetyGuard.isTraversal("C:\\windows\\x"));
|
||||
assertTrue(PathSafetyGuard.isTraversal("D:/escape"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outsideRootIsRejected() throws Exception {
|
||||
Path outside = tempDir.getParent().resolve("outside-" + System.nanoTime() + ".tmp");
|
||||
Files.writeString(outside, "x");
|
||||
try {
|
||||
assertFalse(PathSafetyGuard.isInside(tempDir.toFile(), outside.toFile()),
|
||||
"根外路径必须拒绝");
|
||||
} finally {
|
||||
Files.deleteIfExists(outside);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void symlinkEscapeIsRejected() throws Exception {
|
||||
Path outside = tempDir.getParent().resolve("symlink-target-" + System.nanoTime() + ".tmp");
|
||||
Files.writeString(outside, "secret");
|
||||
Path link = tempDir.resolve("link.tmp");
|
||||
try {
|
||||
Files.createSymbolicLink(link, outside);
|
||||
assertFalse(PathSafetyGuard.isInside(tempDir.toFile(), link.toFile()),
|
||||
"符号链接指向根外必须拒绝(toRealPath 解析)");
|
||||
} catch (UnsupportedOperationException | java.io.IOException ex) {
|
||||
// 平台不支持符号链接时跳过
|
||||
} finally {
|
||||
Files.deleteIfExists(link);
|
||||
Files.deleteIfExists(outside);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void rootItselfIsNotDeletable() {
|
||||
assertFalse(PathSafetyGuard.isInside(tempDir.toFile(), tempDir.toFile()),
|
||||
"根目录本身不算内部(禁止删根)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullAndBlankAreSafe() {
|
||||
assertFalse(PathSafetyGuard.isInside(null, null));
|
||||
assertFalse(PathSafetyGuard.isInside(tempDir.toFile(), null));
|
||||
assertFalse(PathSafetyGuard.isInside(null, tempDir.toFile()));
|
||||
assertTrue(PathSafetyGuard.isTraversal(null));
|
||||
assertTrue(PathSafetyGuard.isTraversal(" "));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user