From 8009845b577d8a2c4bbf4fdd8e8913799a714be6 Mon Sep 17 00:00:00 2001 From: zhouhao Date: Mon, 26 Jan 2026 14:13:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(file):=20=E4=BF=AE=E5=A4=8D=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=8A=E4=BC=A0=E8=B7=AF=E5=BE=84=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E6=BC=8F=E6=B4=9E=EF=BC=8C=E5=A2=9E=E5=BC=BA=E5=AF=B9=E9=9D=9E?= =?UTF-8?q?=E6=B3=95=E6=96=87=E4=BB=B6=E5=90=8D=E7=9A=84=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #344 --- .../web/file/FileUploadProperties.java | 40 +++++-- .../web/file/FileUploadPropertiesTest.java | 104 +++++++++++++++--- 2 files changed, 123 insertions(+), 21 deletions(-) diff --git a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileUploadProperties.java b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileUploadProperties.java index 4cd67adfa..007fbd694 100644 --- a/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileUploadProperties.java +++ b/hsweb-system/hsweb-system-file/src/main/java/org/hswebframework/web/file/FileUploadProperties.java @@ -4,18 +4,21 @@ import lombok.Getter; import lombok.Setter; import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.utils.time.DateFormatter; +import org.hswebframework.web.authorization.exception.AccessDenyException; import org.hswebframework.web.id.IDGenerator; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.http.MediaType; import java.io.File; -import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFilePermission; -import java.util.Collections; +import java.text.Normalizer; import java.util.Date; import java.util.Locale; import java.util.Set; @@ -92,28 +95,49 @@ public class FileUploadProperties { return defaultDeny; } + public static String resolveExtension(String name) { + int lastIndex = name.lastIndexOf("."); + if (lastIndex < 0) { + return ""; + } + return name.substring(lastIndex).toLowerCase(Locale.ROOT); + } + public StaticFileInfo createStaticSavePath(String name) { String fileName = IDGenerator.SNOW_FLAKE_STRING.generate(); String filePath = DateFormatter.toString(new Date(), "yyyyMMdd"); + try { + name = Paths + .get(Normalizer + .normalize(name, Normalizer.Form.NFKC) + .replace("\\", "/")) + .toFile() + .getName(); + } catch (InvalidPathException e) { + throw new AccessDenyException.NoStackTrace(); + } //文件后缀 - String suffix = name.contains(".") ? - name.substring(name.lastIndexOf(".")) : ""; + String suffix = resolveExtension(name); StaticFileInfo info = new StaticFileInfo(); - if (useOriginalFileName) { + // 仅支持 字母数字组成的文件名 + if (useOriginalFileName && name.matches("^[a-zA-Z0-9._-]+$")) { filePath = filePath + "/" + fileName; fileName = name; } else { fileName = fileName + suffix; } String absPath = staticFilePath.concat("/").concat(filePath); - new File(absPath).mkdirs(); - info.location = staticLocation + "/" + filePath + "/" + fileName; - info.savePath = absPath + "/" + fileName; + boolean ignore = new File(absPath).mkdirs(); + + Path fullPath = Paths.get(absPath, fileName); + info.savePath = fullPath.normalize().toString(); + info.relativeLocation = filePath + "/" + fileName; + info.location = staticLocation + "/" + filePath + "/" + fileName; return info; } diff --git a/hsweb-system/hsweb-system-file/src/test/java/org/hswebframework/web/file/FileUploadPropertiesTest.java b/hsweb-system/hsweb-system-file/src/test/java/org/hswebframework/web/file/FileUploadPropertiesTest.java index d72161081..6f1bee584 100644 --- a/hsweb-system/hsweb-system-file/src/test/java/org/hswebframework/web/file/FileUploadPropertiesTest.java +++ b/hsweb-system/hsweb-system-file/src/test/java/org/hswebframework/web/file/FileUploadPropertiesTest.java @@ -1,8 +1,10 @@ package org.hswebframework.web.file; +import org.hswebframework.web.authorization.exception.AccessDenyException; import org.junit.Test; import org.springframework.http.MediaType; +import java.text.Normalizer; import java.util.Arrays; import java.util.HashSet; @@ -12,17 +14,17 @@ public class FileUploadPropertiesTest { @Test - public void testNoSet(){ - FileUploadProperties uploadProperties=new FileUploadProperties(); + public void testNoSet() { + FileUploadProperties uploadProperties = new FileUploadProperties(); assertFalse(uploadProperties.denied("test.xls", MediaType.ALL)); assertFalse(uploadProperties.denied("test.exe", MediaType.ALL)); } @Test - public void testDenyWithAllow(){ - FileUploadProperties uploadProperties=new FileUploadProperties(); - uploadProperties.setAllowFiles(new HashSet<>(Arrays.asList("xls","json"))); + public void testDenyWithAllow() { + FileUploadProperties uploadProperties = new FileUploadProperties(); + uploadProperties.setAllowFiles(new HashSet<>(Arrays.asList("xls", "json"))); assertFalse(uploadProperties.denied("test.xls", MediaType.ALL)); assertFalse(uploadProperties.denied("test.XLS", MediaType.ALL)); @@ -31,9 +33,9 @@ public class FileUploadPropertiesTest { } @Test - public void testDenyWithAllowMediaType(){ - FileUploadProperties uploadProperties=new FileUploadProperties(); - uploadProperties.setAllowMediaType(new HashSet<>(Arrays.asList("application/xls","application/json"))); + public void testDenyWithAllowMediaType() { + FileUploadProperties uploadProperties = new FileUploadProperties(); + uploadProperties.setAllowMediaType(new HashSet<>(Arrays.asList("application/xls", "application/json"))); assertFalse(uploadProperties.denied("test.json", MediaType.APPLICATION_JSON)); @@ -41,10 +43,9 @@ public class FileUploadPropertiesTest { } - @Test - public void testDenyWithDenyMediaType(){ - FileUploadProperties uploadProperties=new FileUploadProperties(); + public void testDenyWithDenyMediaType() { + FileUploadProperties uploadProperties = new FileUploadProperties(); uploadProperties.setDenyMediaType(new HashSet<>(Arrays.asList("application/json"))); assertFalse(uploadProperties.denied("test.xls", MediaType.ALL)); @@ -52,9 +53,10 @@ public class FileUploadPropertiesTest { assertTrue(uploadProperties.denied("test.exe", MediaType.APPLICATION_JSON)); } + @Test - public void testDenyWithDeny(){ - FileUploadProperties uploadProperties=new FileUploadProperties(); + public void testDenyWithDeny() { + FileUploadProperties uploadProperties = new FileUploadProperties(); uploadProperties.setDenyFiles(new HashSet<>(Arrays.asList("exe"))); assertFalse(uploadProperties.denied("test.xls", MediaType.ALL)); @@ -64,4 +66,80 @@ public class FileUploadPropertiesTest { } + @Test + // https://github.com/hs-web/hsweb-framework/issues/344 + public void testIllegalFileName() { + FileUploadProperties uploadProperties = new FileUploadProperties(); + uploadProperties.setUseOriginalFileName(true); + + // 基本的路径遍历攻击 + FileUploadProperties.StaticFileInfo fileInfo = uploadProperties + .createStaticSavePath("../../../../pom.xml"); + assertFalse(fileInfo.getSavePath().contains("../")); + assertFalse(fileInfo.getRelativeLocation().contains("../")); + assertFalse(fileInfo.getLocation().contains("../")); + + // Windows风格的路径遍历攻击 + fileInfo = uploadProperties.createStaticSavePath("..\\..\\..\\..\\pom.xml"); + assertFalse(fileInfo.getSavePath().contains("..\\")); + assertFalse(fileInfo.getRelativeLocation().contains("..\\")); + assertFalse(fileInfo.getLocation().contains("..\\")); + + // URL编码的路径遍历 + fileInfo = uploadProperties.createStaticSavePath("..%2F..%2F..%2F..%2Fpom.xml"); + assertFalse(fileInfo.getSavePath().contains("../")); + assertFalse(fileInfo.getSavePath().contains("..%2F")); + assertFalse(fileInfo.getRelativeLocation().contains("../")); + assertFalse(fileInfo.getLocation().contains("../")); + + // 双重URL编码 + fileInfo = uploadProperties.createStaticSavePath("..%252F..%252F..%252Fpom.xml"); + assertFalse(fileInfo.getSavePath().contains("../")); + assertFalse(fileInfo.getSavePath().contains("..%2F")); + assertFalse(fileInfo.getSavePath().contains("..%252F")); + + // Unicode编码的路径遍历 + fileInfo = uploadProperties.createStaticSavePath("..%c0%af..%c0%afpom.xml"); + assertFalse(fileInfo.getSavePath().contains("../")); + assertFalse(fileInfo.getRelativeLocation().contains("../")); + + // 绝对路径攻击 - Linux + fileInfo = uploadProperties.createStaticSavePath("/etc/passwd"); + assertFalse(fileInfo.getSavePath().startsWith("/etc/")); + assertFalse(fileInfo.getLocation().contains("/etc/passwd")); + + // 绝对路径攻击 - Windows + fileInfo = uploadProperties.createStaticSavePath("C:\\Windows\\System32\\config\\sam"); + assertFalse(fileInfo.getSavePath().contains("C:\\")); + assertFalse(fileInfo.getSavePath().contains("System32")); + + // 混合斜杠 + fileInfo = uploadProperties.createStaticSavePath("..\\../..\\../pom.xml"); + assertFalse(fileInfo.getSavePath().contains("../")); + assertFalse(fileInfo.getSavePath().contains("..\\")); + + // 过度的路径遍历 + fileInfo = uploadProperties.createStaticSavePath("../../../../../../../../../../../../etc/passwd"); + assertFalse(fileInfo.getSavePath().contains("../")); + assertFalse(fileInfo.getLocation().contains("/etc/")); + + +// // 带有空字节注入 + assertThrows(AccessDenyException.class, + ()->{ + uploadProperties.createStaticSavePath("../../pom.xml\0.jpg"); + }); + + // 点和斜杠的各种组合 + fileInfo = uploadProperties.createStaticSavePath("....//....//pom.xml"); + assertFalse(fileInfo.getSavePath().contains("..")); + assertFalse(fileInfo.getSavePath().contains("//")); + + // 反斜杠编码 + fileInfo = uploadProperties.createStaticSavePath("..%5c..%5cpom.xml"); + assertFalse(fileInfo.getSavePath().contains("..\\")); + assertFalse(fileInfo.getSavePath().contains("..%5c")); + } + + } \ No newline at end of file