diff --git a/java/src/main/java/com/yfd/platform/modules/specialDocument/service/impl/NodesServiceImpl.java b/java/src/main/java/com/yfd/platform/modules/specialDocument/service/impl/NodesServiceImpl.java index 80f8f07..48af12c 100644 --- a/java/src/main/java/com/yfd/platform/modules/specialDocument/service/impl/NodesServiceImpl.java +++ b/java/src/main/java/com/yfd/platform/modules/specialDocument/service/impl/NodesServiceImpl.java @@ -46,6 +46,7 @@ import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.io.*; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; @@ -958,7 +959,7 @@ public class NodesServiceImpl extends ServiceImpl implements //查询项目信息 Project project = projectMapper.selectById(id); - if(!project.getProjectName().equals(zipName)){ + if (!project.getProjectName().equals(zipName)) { throw new RuntimeException("压缩包名称需要和项目名称保持一致"); } @@ -1372,86 +1373,109 @@ public class NodesServiceImpl extends ServiceImpl implements int dotIndex = fileName.lastIndexOf('.'); return (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex); } + //java.nio.file.Files private File unzipFile(Path sourcePath, String baseDir) throws IOException { - // 创建目标目录 - Path destRoot = Paths.get(baseDir); + Path destRoot = Paths.get(baseDir).normalize(); java.nio.file.Files.createDirectories(destRoot); - try (ZipInputStream zis = new ZipInputStream( - java.nio.file.Files.newInputStream(sourcePath), StandardCharsets.UTF_8)) { - - ZipEntry entry = null; - - while (true) { // 循环结构改为无限循环 - try { - entry = zis.getNextEntry(); // 尝试获取下一个条目 - if (entry == null) { - break; // 没有更多条目,退出循环 - } - - // 打印当前处理的条目名称 - LOGGER.info("Processing ZIP entry: {}", entry.getName()); - - // 跳过 macOS 系统文件 - if (entry.getName().startsWith("__MACOSX")) { - continue; - } - - // 标准化路径并处理目录标识 - String entryName = entry.getName() - .replace("\\", "/") - .replaceFirst("^/+", ""); // 去除开头的斜杠 - - // 检测是否为目录(兼容以'/'结尾的条目) - boolean isDirectory = entry.isDirectory() || entry.getName().endsWith("/"); - - // 调整路径:去除顶层目录(假设所有文件在单一顶层目录下) - 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); // 确保路径安全 - - // 处理目录 - if (isDirectory) { - java.nio.file.Files.createDirectories(targetPath); - } else { - // 处理文件 - 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(); // 关闭当前条目 - } - + // 尝试UTF-8和GBK编码 + Charset[] charsets = {StandardCharsets.UTF_8, Charset.forName("GBK")}; + for (Charset charset : charsets) { + try (ZipInputStream zis = new ZipInputStream(java.nio.file.Files.newInputStream(sourcePath), charset)) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + processZipEntry(zis, entry, destRoot); } + return destRoot.toFile(); // 解压成功直接返回 + } catch (IllegalArgumentException | IOException e) { + LOGGER.debug("编码 {} 解压失败,尝试下一个编码", charset, e); } } + throw new IOException("无法使用UTF-8或GBK编码解压文件"); + } - return destRoot.toFile(); + private void processZipEntry(ZipInputStream zis, ZipEntry entry, Path destRoot) throws IOException { + // 1. 跳过所有系统文件和隐藏文件 + if (shouldSkipEntry(entry)) { + LOGGER.debug("跳过系统文件:{}", entry.getName()); + return; + } + + // 2. 消毒文件名并构建安全路径 + String sanitizedName = sanitizeFileName(entry.getName()); + 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("^/+", ""); // 去除开头的斜杠 + + // 防御路径穿越攻击 + sanitized = sanitized.replaceAll("\\.\\./", "_"); + return sanitized; + } + + /** + * 构建安全路径 + */ + private Path buildSafePath(Path destRoot, String sanitizedName) throws IOException { + Path targetPath = destRoot.resolve(sanitizedName).normalize(); + + // 二次路径校验 + if (!targetPath.startsWith(destRoot)) { + 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 { + LOGGER.warn("文件已存在,跳过覆盖:{}", targetPath); + } }