From 0e4d680aefd22cc649bd75b83aff89d2179d3e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Wed, 2 Sep 2026 07:25:49 +0800 Subject: [PATCH] =?UTF-8?q?task-162:=20=E8=B7=AF=E5=BE=84=E7=A9=BF?= =?UTF-8?q?=E8=B6=8A=E8=BE=93=E5=85=A5=E7=9F=A9=E9=98=B5=EF=BC=88../?= =?UTF-8?q?=E3=80=81..\=E3=80=81=E7=BB=9D=E5=AF=B9=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E3=80=81%2e=20=E7=BC=96=E7=A0=81=E3=80=81=E5=85=A8=E8=A7=92?= =?UTF-8?q?=E7=82=B9=E3=80=81=E6=B7=B7=E5=90=88=E5=88=86=E9=9A=94=E7=AC=A6?= =?UTF-8?q?=E3=80=81=E7=A9=BA=E5=AD=97=E8=8A=82=E5=85=A8=E9=83=A8=E6=8B=92?= =?UTF-8?q?=E7=BB=9D=EF=BC=89+=209=20=E6=9D=A1=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/file/service/PathSafetyGuard.java | 10 ++- .../file/service/PathTraversalMatrixTest.java | 71 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/file/service/PathTraversalMatrixTest.java 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")); + } +}