提交代码
This commit is contained in:
parent
7c2f76acac
commit
19e7240853
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user