Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
Expand Down Expand Up @@ -169,8 +170,8 @@ private long decompress(ZipFile zip, ZipEntry entry, File target, long maxSize)
int part;
while ((part = in.read(buffer, 0, buffer.length)) >= 0) {
if ((compressed += part) > maxSize) {
throw new SecurityException(StringUtils.format("The file to unzip is too large. [file={0}, "
+ "max={1}]",
String template = "The file to unzip is too large. [file={0}, max={1}]";
throw new SecurityException(StringUtils.format(template,
this.file().getName(),
this.security.getCompressedTotalSize()));
}
Expand Down Expand Up @@ -202,26 +203,47 @@ private File getTarget(ZipEntry entry) {
return this.getActualTarget(redirect.redirectedFile);
}
}

String name = entry.getName();
File actualTarget = new File(name);
return this.getActualTarget(actualTarget);

// 检查是否包含绝对路径字符(以'/'或驱动器字母开头)
if (name.startsWith("/") || name.startsWith("\\") || (name.length() > 1 && name.charAt(1) == ':')) {
if (!this.security.isCrossPath()) {
throw new SecurityException(StringUtils.format("Detected a potential path traversal attack. [path={0}]",
name));
}
}

Path targetDir = this.getTargetDirectory().toPath().normalize();
Path targetPath = targetDir.resolve(name).normalize();

// 使用Path.startsWith进行前缀检查(比String.startsWith更安全)
if (!this.security.isCrossPath() && !targetPath.startsWith(targetDir)) {
throw new SecurityException(StringUtils.format("Detected a potential path traversal attack. [path={0}]",
name));
}

return targetPath.toFile();
}

private File getActualTarget(File target) {
File actual = target;
if (!target.isAbsolute()) {
actual = new File(this.getTargetDirectory(), target.getPath());
// 如果是绝对路径,redirector明确指定,直接返回(redirector的职责)
if (target.isAbsolute()) {
return FileUtils.canonicalize(target);
}
actual = FileUtils.canonicalize(actual);

Path targetDir = this.getTargetDirectory().toPath().normalize();
Path actual = targetDir.resolve(target.toPath()).normalize();

if (this.security.isCrossPath()) {
return actual;
return actual.toFile();
}
File targetDirectory = FileUtils.canonicalize(this.getTargetDirectory());
if (!actual.getPath().startsWith(targetDirectory.getPath())) {
throw new SecurityException(
StringUtils.format("Detected a potential path traversal attack. [path={0}]", target.getPath()));

if (!actual.startsWith(targetDir)) {
throw new SecurityException(StringUtils.format("Detected a potential path traversal attack. [path={0}]",
target.getPath()));
}
return actual;
return actual.toFile();
}

private File getTargetDirectory() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) 2024 Huawei Technologies Co., Ltd. All rights reserved.
* This file is a part of the ModelEngine Project.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
* Copyright (c) 2024-2025 Huawei Technologies Co., Ltd. All rights reserved.
* This file is a part of the ModelEngine Project.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

package modelengine.fitframework.util.support;

Expand Down Expand Up @@ -195,20 +195,134 @@ void givenMax1ByteSecurityThenThrowException() {
@Test
@DisplayName("给定压缩包中存在遍历路径文件,解压失败。")
public void givenPathTraversalThenCatchException() throws IOException {
File testZipFile = new File("test-archive.zip");
File targetDir = new File("target-dir");
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(testZipFile.toPath()))) {
ZipEntry entry = new ZipEntry("../unauthorized-file.txt");
zos.putNextEntry(entry);
zos.write("Malicious content".getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
File testZipFile = new File("src/test/resources/zip-slip-test/test-archive.zip");
File targetDir = new File("src/test/resources/zip-slip-test/target-dir");
try {
// 确保父目录存在
FileUtils.ensureDirectory(testZipFile.getParentFile());
// 动态创建包含恶意路径的ZIP文件
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(testZipFile.toPath()))) {
ZipEntry entry = new ZipEntry("../unauthorized-file.txt");
zos.putNextEntry(entry);
zos.write("Malicious content".getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}

Unzip unzip =
FileUtils.unzip(testZipFile).secure(new Unzip.Security(100, 1024, false)).target(targetDir);
SecurityException securityException = catchThrowableOfType(SecurityException.class, unzip::start);
assertThat(securityException.getMessage()).startsWith("Detected a potential path traversal attack. ");
} finally {
FileUtils.delete(testZipFile);
FileUtils.delete(targetDir);
}
}

@Test
@DisplayName("Given zip entry with multiple parent path traversals then throw SecurityException")
public void givenMultipleParentPathTraversalsThenThrowSecurityException() throws IOException {
File testZipFile = new File("src/test/resources/zip-slip-test/test-archive-multi.zip");
File targetDir = new File("src/test/resources/zip-slip-test/target-dir-multi");
try {
// 确保父目录存在
FileUtils.ensureDirectory(testZipFile.getParentFile());
// 动态创建包含多级路径遍历的ZIP文件
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(testZipFile.toPath()))) {
ZipEntry entry = new ZipEntry("../../../../../../etc/passwd");
zos.putNextEntry(entry);
zos.write("Malicious content".getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}

Unzip unzip =
FileUtils.unzip(testZipFile).secure(new Unzip.Security(100, 1024, false)).target(targetDir);
SecurityException securityException = catchThrowableOfType(SecurityException.class, unzip::start);
assertThat(securityException.getMessage()).contains("Detected a potential path traversal attack");
} finally {
FileUtils.delete(testZipFile);
FileUtils.delete(targetDir);
}
}

Unzip unzip = FileUtils.unzip(testZipFile).secure(new Unzip.Security(100, 1024, false)).target(targetDir);
SecurityException securityException = catchThrowableOfType(SecurityException.class, unzip::start);
assertThat(securityException.getMessage()).startsWith("Detected a potential path traversal attack. ");
FileUtils.delete(testZipFile);
FileUtils.delete(targetDir);
@Test
@DisplayName("Given zip entry with absolute path then throw SecurityException")
public void givenAbsolutePathEntryThenThrowSecurityException() throws IOException {
File testZipFile = new File("src/test/resources/zip-slip-test/test-archive-absolute.zip");
File targetDir = new File("src/test/resources/zip-slip-test/target-dir-absolute");
try {
// 确保父目录存在
FileUtils.ensureDirectory(testZipFile.getParentFile());
// 动态创建包含绝对路径的ZIP文件
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(testZipFile.toPath()))) {
ZipEntry entry = new ZipEntry("/tmp/unauthorized-file.txt");
zos.putNextEntry(entry);
zos.write("Malicious content".getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}

Unzip unzip =
FileUtils.unzip(testZipFile).secure(new Unzip.Security(100, 1024, false)).target(targetDir);
SecurityException securityException = catchThrowableOfType(SecurityException.class, unzip::start);
assertThat(securityException.getMessage()).contains("Detected a potential path traversal attack");
} finally {
FileUtils.delete(testZipFile);
FileUtils.delete(targetDir);
}
}

@Test
@DisplayName("Given zip entry with path traversal in middle then throw SecurityException")
public void givenPathTraversalInMiddleThenThrowSecurityException() throws IOException {
File testZipFile = new File("src/test/resources/zip-slip-test/test-archive-middle.zip");
File targetDir = new File("src/test/resources/zip-slip-test/target-dir-middle");
try {
// 确保父目录存在
FileUtils.ensureDirectory(testZipFile.getParentFile());
// 动态创建包含中间路径遍历的ZIP文件
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(testZipFile.toPath()))) {
ZipEntry entry = new ZipEntry("subdir/../../../unauthorized.txt");
zos.putNextEntry(entry);
zos.write("Malicious content".getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}

Unzip unzip =
FileUtils.unzip(testZipFile).secure(new Unzip.Security(100, 1024, false)).target(targetDir);
SecurityException securityException = catchThrowableOfType(SecurityException.class, unzip::start);
assertThat(securityException.getMessage()).contains("Detected a potential path traversal attack");
} finally {
FileUtils.delete(testZipFile);
FileUtils.delete(targetDir);
}
}

@Test
@DisplayName("Given zip entry with safe nested path then unzip successfully")
public void givenSafeNestedPathThenUnzipSuccessfully() throws IOException {
File testZipFile = new File("src/test/resources/zip-slip-test/test-archive-safe.zip");
File targetDir = new File("src/test/resources/zip-slip-test/target-dir-safe");
try {
// 确保父目录存在
FileUtils.ensureDirectory(testZipFile.getParentFile());
// 动态创建包含安全嵌套路径的ZIP文件
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(testZipFile.toPath()))) {
ZipEntry entry = new ZipEntry("subdir/nested/safe-file.txt");
zos.putNextEntry(entry);
zos.write("Safe content".getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}

Unzip unzip =
FileUtils.unzip(testZipFile).secure(new Unzip.Security(100, 1024, false)).target(targetDir);
assertThatNoException().isThrownBy(unzip::start);

File safeFile = new File(targetDir, "subdir/nested/safe-file.txt");
assertThat(safeFile).exists();
assertThat(Files.readString(safeFile.toPath())).isEqualTo("Safe content");
} finally {
FileUtils.delete(testZipFile);
FileUtils.delete(targetDir);
}
}

@Test
Expand Down