diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/PathSafetyGuard.java b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/PathSafetyGuard.java index 6cf1ca7f..b3ca19aa 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/PathSafetyGuard.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/PathSafetyGuard.java @@ -33,15 +33,21 @@ public final class PathSafetyGuard { return childPath.startsWith(rootPath) && !childPath.equals(rootPath); } - /** 路径名是否含穿越特征(../、绝对路径、盘符、反斜杠)。 */ + /** + * 路径名是否含穿越特征(../、..\\、绝对路径、盘符、URL 编码点 %2e、 + * 全角点 .、混合分隔符、空字节)。 + */ public static boolean isTraversal(String name) { if (name == null || name.isBlank()) { return true; } return name.contains("..") + || name.contains("%2e") || name.contains("%2E") + || name.contains(".") || name.startsWith("/") || name.matches("^[A-Za-z]:.*") - || name.contains("\\"); + || name.contains("\\") + || name.contains("\0"); } private static Path resolve(File file) { diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/file/service/PathTraversalMatrixTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/file/service/PathTraversalMatrixTest.java new file mode 100644 index 00000000..9a4b7f46 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/file/service/PathTraversalMatrixTest.java @@ -0,0 +1,71 @@ +package com.nanri.aiimage.modules.file.service; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * task-162:路径穿越输入矩阵(plan 09)。 + * ../、..\\、绝对路径、URL 编码 %2e%2e、Unicode 全角点、混合分隔符、空字节 + * 全部拒绝;普通文件名放行。 + */ +class PathTraversalMatrixTest { + + @Test + void dotdotSlashRejected() { + assertTrue(PathSafetyGuard.isTraversal("../escape")); + assertTrue(PathSafetyGuard.isTraversal("a/../../b")); + assertTrue(PathSafetyGuard.isTraversal("..../x")); + } + + @Test + void dotdotBackslashRejected() { + assertTrue(PathSafetyGuard.isTraversal("..\\escape")); + assertTrue(PathSafetyGuard.isTraversal("a\\..\\b")); + } + + @Test + void urlEncodedDotdotRejected() { + assertTrue(PathSafetyGuard.isTraversal("%2e%2e/x")); + assertTrue(PathSafetyGuard.isTraversal("%2E%2E/x")); + assertTrue(PathSafetyGuard.isTraversal("a%2e%2eb")); + } + + @Test + void fullwidthDotRejected() { + assertTrue(PathSafetyGuard.isTraversal("\uFF0E\uFF0E/escape"), "全角点 .. 拒绝"); + assertTrue(PathSafetyGuard.isTraversal("a\uFF0E\uFF0Eb")); + } + + @Test + void absoluteWindowsPathRejected() { + assertTrue(PathSafetyGuard.isTraversal("C:\\windows\\system32")); + assertTrue(PathSafetyGuard.isTraversal("D:/escape/file")); + } + + @Test + void absoluteUnixPathRejected() { + assertTrue(PathSafetyGuard.isTraversal("/etc/passwd")); + assertTrue(PathSafetyGuard.isTraversal("/tmp/escape")); + } + + @Test + void mixedSeparatorsRejected() { + assertTrue(PathSafetyGuard.isTraversal("..\\/escape")); + assertTrue(PathSafetyGuard.isTraversal("a/..\\b/c")); + assertTrue(PathSafetyGuard.isTraversal("C:..\\x")); + } + + @Test + void nullByteRejected() { + assertTrue(PathSafetyGuard.isTraversal("evil\0.tmp")); + } + + @Test + void normalNamesAllowed() { + assertFalse(PathSafetyGuard.isTraversal("0123456789abcdef0123456789abcdef.xlsx")); + assertFalse(PathSafetyGuard.isTraversal("result-2026-09-02.xlsx")); + assertFalse(PathSafetyGuard.isTraversal("chunk-1.json")); + } +}