Skip to content
Open
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
23 changes: 22 additions & 1 deletion base/src/main/java/com/tinyengine/it/common/utils/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,34 @@ private static File convertMultipartFileToFile(MultipartFile multipartFile) thro
private static List<FileInfo> processZipEntries(ZipInputStream zis, File tempDir) throws IOException {
List<FileInfo> fileInfoList = new ArrayList<>();
ZipEntry zipEntry;

// 将 tempDir 转为规范路径(例如解析符号链接、父目录等)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

补上测试用例

Path safeDir = tempDir.toPath().toRealPath();
log.info("Created temporary directory at: {}, real path: {}", tempDir.getAbsolutePath(), safeDir);
while ((zipEntry = zis.getNextEntry()) != null) {

// 获取 ZIP 条目中的路径(可能包含 ../ 或绝对路径)
String entryName = zipEntry.getName();

// 拼接并规范化路径
Path targetPath = safeDir.resolve(entryName).normalize();

log.info("Processing ZIP entry: {}, target path: {}", entryName, targetPath);

// 关键校验:确保目标路径仍在 safeDir 之下
if (!targetPath.startsWith(safeDir)) {
throw new SecurityException("检测到跨目录攻击: " + entryName);
}
File newFile = new File(tempDir, zipEntry.getName());

if (zipEntry.isDirectory()) {
// 创建目录(同时确保父目录存在)
Files.createDirectories(targetPath);
fileInfoList.add(new FileInfo(newFile.getName(), "", true)); // 添加目录
} else {
// 确保父目录存在
if(targetPath.getParent() != null) {
Files.createDirectories(targetPath.getParent());
}
extractFile(zis, newFile); // 解压文件
fileInfoList.add(new FileInfo(newFile.getName(), readFileContent(newFile), false)); // 添加文件内容
Comment on lines 242 to 254
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify path-validation path vs extraction path usage in processZipEntries.
rg -n 'Path targetPath = safeDir\.resolve\(entryName\)\.normalize\(\);|File newFile = new File\(tempDir, zipEntry\.getName\(\)\);|extractFile\(zis, newFile\);|readFileContent\(newFile\)' base/src/main/java/com/tinyengine/it/common/utils/Utils.java

Repository: opentiny/tiny-engine-backend-java

Length of output: 392


🏁 Script executed:

#!/bin/bash
# Get broader context around the issue area
sed -n '220,260p' base/src/main/java/com/tinyengine/it/common/utils/Utils.java

Repository: opentiny/tiny-engine-backend-java

Length of output: 1695


🏁 Script executed:

#!/bin/bash
# Find where safeDir is defined/initialized
rg -n 'safeDir\s*=' base/src/main/java/com/tinyengine/it/common/utils/Utils.java -B 2 -A 2

Repository: opentiny/tiny-engine-backend-java

Length of output: 386


🏁 Script executed:

#!/bin/bash
# Find the method signature to understand the full context
rg -n 'void processZipEntries|Path.*processZipEntries|private.*processZipEntries' base/src/main/java/com/tinyengine/it/common/utils/Utils.java -A 15

Repository: opentiny/tiny-engine-backend-java

Length of output: 878


🏁 Script executed:

#!/bin/bash
# Find extractFile and readFileContent method signatures
rg -n 'private.*extractFile\(|void extractFile\(|public.*extractFile\(' base/src/main/java/com/tinyengine/it/common/utils/Utils.java -A 3

Repository: opentiny/tiny-engine-backend-java

Length of output: 406


🏁 Script executed:

#!/bin/bash
# Find readFileContent method
rg -n 'private.*readFileContent\(|String readFileContent\(' base/src/main/java/com/tinyengine/it/common/utils/Utils.java -A 3

Repository: opentiny/tiny-engine-backend-java

Length of output: 289


Use the validated targetPath as the only extraction/read path.

You validate targetPath against safeDir with path normalization and a security check, but then extract/read via new File(tempDir, zipEntry.getName()). Since safeDir is created via toRealPath() (which resolves symlinks), while newFile uses the raw tempDir, these paths can diverge if tempDir is a symlink, allowing the validation to be bypassed.

Suggested patch
-            File newFile = new File(tempDir, zipEntry.getName());
+            File newFile = targetPath.toFile();

             if (zipEntry.isDirectory()) {
                 // 创建目录(同时确保父目录存在)
                 Files.createDirectories(targetPath);
                 fileInfoList.add(new FileInfo(newFile.getName(), "", true));  // 添加目录
             } else {
                 // 确保父目录存在
-                Files.createDirectories(targetPath.getParent());
+                Path parent = targetPath.getParent();
+                if (parent != null) {
+                    Files.createDirectories(parent);
+                }
                 extractFile(zis, newFile);  // 解压文件
                 fileInfoList.add(new FileInfo(newFile.getName(), readFileContent(newFile), false));  // 添加文件内容
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
File newFile = new File(tempDir, zipEntry.getName());
if (zipEntry.isDirectory()) {
// 创建目录(同时确保父目录存在)
Files.createDirectories(targetPath);
fileInfoList.add(new FileInfo(newFile.getName(), "", true)); // 添加目录
} else {
// 确保父目录存在
Files.createDirectories(targetPath.getParent());
extractFile(zis, newFile); // 解压文件
fileInfoList.add(new FileInfo(newFile.getName(), readFileContent(newFile), false)); // 添加文件内容
File newFile = targetPath.toFile();
if (zipEntry.isDirectory()) {
// 创建目录(同时确保父目录存在)
Files.createDirectories(targetPath);
fileInfoList.add(new FileInfo(newFile.getName(), "", true)); // 添加目录
} else {
// 确保父目录存在
Path parent = targetPath.getParent();
if (parent != null) {
Files.createDirectories(parent);
}
extractFile(zis, newFile); // 解压文件
fileInfoList.add(new FileInfo(newFile.getName(), readFileContent(newFile), false)); // 添加文件内容
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@base/src/main/java/com/tinyengine/it/common/utils/Utils.java` around lines
242 - 252, The code validates targetPath against safeDir but then creates and
uses new File(tempDir, zipEntry.getName()), which can bypass the
normalization/symlink check; update the extraction/read logic in Utils (the
unzip/unpack method) to use the already-validated targetPath for all filesystem
operations: when creating directories, extracting entries, and reading contents,
call Files.createDirectories(targetPath) /
Files.createDirectories(targetPath.getParent()), pass targetPath (or
targetPath.toFile() if extractFile/readFileContent require File) into
extractFile and readFileContent instead of newFile, and remove the new
File(tempDir, ...) usage so all IO goes through the validated Path.

}
Expand Down
Loading