提交代码

This commit is contained in:
lilin 2025-05-24 13:37:41 +08:00
parent 7c2f76acac
commit 19e7240853

View File

@ -46,6 +46,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.*; import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@ -958,7 +959,7 @@ public class NodesServiceImpl extends ServiceImpl<NodesMapper, Nodes> implements
//查询项目信息 //查询项目信息
Project project = projectMapper.selectById(id); Project project = projectMapper.selectById(id);
if(!project.getProjectName().equals(zipName)){ if (!project.getProjectName().equals(zipName)) {
throw new RuntimeException("压缩包名称需要和项目名称保持一致"); throw new RuntimeException("压缩包名称需要和项目名称保持一致");
} }
@ -1372,86 +1373,109 @@ public class NodesServiceImpl extends ServiceImpl<NodesMapper, Nodes> implements
int dotIndex = fileName.lastIndexOf('.'); int dotIndex = fileName.lastIndexOf('.');
return (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex); return (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex);
} }
//java.nio.file.Files
private File unzipFile(Path sourcePath, String baseDir) throws IOException { private File unzipFile(Path sourcePath, String baseDir) throws IOException {
// 创建目标目录 Path destRoot = Paths.get(baseDir).normalize();
Path destRoot = Paths.get(baseDir);
java.nio.file.Files.createDirectories(destRoot); java.nio.file.Files.createDirectories(destRoot);
try (ZipInputStream zis = new ZipInputStream( // 尝试UTF-8和GBK编码
java.nio.file.Files.newInputStream(sourcePath), StandardCharsets.UTF_8)) { Charset[] charsets = {StandardCharsets.UTF_8, Charset.forName("GBK")};
for (Charset charset : charsets) {
ZipEntry entry = null; try (ZipInputStream zis = new ZipInputStream(java.nio.file.Files.newInputStream(sourcePath), charset)) {
ZipEntry entry;
while (true) { // 循环结构改为无限循环 while ((entry = zis.getNextEntry()) != null) {
try { processZipEntry(zis, entry, destRoot);
entry = zis.getNextEntry(); // 尝试获取下一个条目 }
if (entry == null) { return destRoot.toFile(); // 解压成功直接返回
break; // 没有更多条目退出循环 } catch (IllegalArgumentException | IOException e) {
LOGGER.debug("编码 {} 解压失败,尝试下一个编码", charset, e);
}
}
throw new IOException("无法使用UTF-8或GBK编码解压文件");
} }
// 打印当前处理的条目名称 private void processZipEntry(ZipInputStream zis, ZipEntry entry, Path destRoot) throws IOException {
LOGGER.info("Processing ZIP entry: {}", entry.getName()); // 1. 跳过所有系统文件和隐藏文件
if (shouldSkipEntry(entry)) {
// 跳过 macOS 系统文件 LOGGER.debug("跳过系统文件:{}", entry.getName());
if (entry.getName().startsWith("__MACOSX")) { return;
continue;
} }
// 标准化路径并处理目录标识 // 2. 消毒文件名并构建安全路径
String entryName = entry.getName() String sanitizedName = sanitizeFileName(entry.getName());
.replace("\\", "/") Path targetPath = buildSafePath(destRoot, sanitizedName);
// 3. 仅处理实际文件跳过空目录
if (!entry.isDirectory()) {
writeFileContent(zis, targetPath);
}
}
/**
* 判断是否需要跳过条目
*/
private boolean shouldSkipEntry(ZipEntry entry) {
String name = entry.getName();
return name.startsWith("__MACOSX/") || // macOS系统文件
name.contains("/.DS_Store") || // macOS资源文件
name.startsWith(".") || // 隐藏文件
name.endsWith("Thumbs.db"); // Windows缩略图文件
}
/**
* 文件名消毒逻辑保留路径分隔符 /
*/
private String sanitizeFileName(String original) {
// 替换非法字符保留 / \
String sanitized = original
.replaceAll("[*?\"<>|\0]", "_") // 仅替换 * ? " < > | 和空字符
.replace("\\", "/") // 统一路径分隔符为 /
.replaceFirst("^/+", ""); // 去除开头的斜杠 .replaceFirst("^/+", ""); // 去除开头的斜杠
// 检测是否为目录兼容以'/'结尾的条目 // 防御路径穿越攻击
boolean isDirectory = entry.isDirectory() || entry.getName().endsWith("/"); sanitized = sanitized.replaceAll("\\.\\./", "_");
return sanitized;
// 调整路径去除顶层目录假设所有文件在单一顶层目录下
if (entryName.contains("/")) {
int firstSlash = entryName.indexOf('/');
entryName = entryName.substring(firstSlash + 1);
// 若处理后名称为空则跳过顶层目录条目
if (entryName.isEmpty() && isDirectory) {
continue;
}
} }
Path targetPath = destRoot.resolve(entryName).normalize(); /**
validatePathSafetya(targetPath, destRoot); // 确保路径安全 * 构建安全路径
*/
private Path buildSafePath(Path destRoot, String sanitizedName) throws IOException {
Path targetPath = destRoot.resolve(sanitizedName).normalize();
// 处理目录 // 二次路径校验
if (isDirectory) { if (!targetPath.startsWith(destRoot)) {
java.nio.file.Files.createDirectories(targetPath); throw new IOException("检测到非法路径穿越:" + targetPath);
}
return targetPath;
}
/**
* 写入文件内容仅在需要时创建目录
*/
private void writeFileContent(ZipInputStream zis, Path targetPath) throws IOException {
// 仅当文件不存在时才写入
if (!java.nio.file.Files.exists(targetPath)) {
// 按需创建父目录
Path parent = targetPath.getParent();
if (parent != null && !java.nio.file.Files.exists(parent)) {
java.nio.file.Files.createDirectories(parent);
}
// 使用缓冲流提升性能
try (BufferedOutputStream os = new BufferedOutputStream(java.nio.file.Files.newOutputStream(targetPath))) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = zis.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
}
LOGGER.debug("成功解压文件:{}", targetPath);
} else { } else {
// 处理文件 LOGGER.warn("文件已存在,跳过覆盖:{}", targetPath);
if (java.nio.file.Files.exists(targetPath)) {
LOGGER.warn("File already exists, skip overwriting: {}", targetPath);
continue;
} }
// 确保父目录存在
java.nio.file.Files.createDirectories(targetPath.getParent());
try (OutputStream os = java.nio.file.Files.newOutputStream(targetPath)) {
IOUtils.copy(zis, os);
}
}
} catch (IllegalArgumentException e) {
// 捕获 MALFORMED 异常并记录条目名称
String errorEntry = (entry != null) ? entry.getName() : "Unknown Entry";
LOGGER.error("MALFORMED ZIP Entry Detected! Entry Name: {}", errorEntry, e);
} finally {
if (entry != null) {
zis.closeEntry(); // 关闭当前条目
}
}
}
}
return destRoot.toFile();
} }