fix(file): 修复文件上传路径解析漏洞,增强对非法文件名的校验

#344
This commit is contained in:
zhouhao
2026-01-26 14:13:24 +08:00
parent 76cb89f56e
commit 8009845b57
2 changed files with 123 additions and 21 deletions

View File

@@ -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;
}

View File

@@ -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"));
}
}