From 8922cbfbb090792eb12171e8805d63f216494d69 Mon Sep 17 00:00:00 2001 From: xingyi Date: Wed, 10 Jun 2026 09:43:19 +0800 Subject: [PATCH] [issue_1199][cve] fix ZipUtil Zip Slip & Zip Bomb Vulnerability Report #1199 --- .../dtstack/taier/common/util/ZipUtil.java | 129 ++++++++++++---- .../taier/common/util/ZipUtilTest.java | 141 ++++++++++++++++++ .../plugin/common/utils/ZipUtil.java | 92 +++++++++--- 3 files changed, 311 insertions(+), 51 deletions(-) create mode 100644 taier-common/src/test/java/com/dtstack/taier/common/util/ZipUtilTest.java diff --git a/taier-common/src/main/java/com/dtstack/taier/common/util/ZipUtil.java b/taier-common/src/main/java/com/dtstack/taier/common/util/ZipUtil.java index 052dd97aec..6c18cf7bff 100644 --- a/taier-common/src/main/java/com/dtstack/taier/common/util/ZipUtil.java +++ b/taier-common/src/main/java/com/dtstack/taier/common/util/ZipUtil.java @@ -18,6 +18,7 @@ package com.dtstack.taier.common.util; +import com.dtstack.taier.common.exception.TaierDefineException; import org.apache.tools.zip.ZipFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,6 +55,12 @@ public class ZipUtil { private static byte[] _byte = new byte[1024]; + private static final int MAX_ZIP_ENTRY_COUNT = 1000; + private static final int MAX_ZIP_RECURSION_DEPTH = 3; + private static final long MAX_ZIP_TOTAL_UNCOMPRESSED_SIZE = 100L * 1024 * 1024; + private static final long MAX_ZIP_ENTRY_UNCOMPRESSED_SIZE = 50L * 1024 * 1024; + private static final long MAX_ZIP_COMPRESSION_RATIO = 100L; + public static byte[] compress(byte[] rowData) { byte[] backData = null; ZipOutputStream zip = null; @@ -224,68 +231,124 @@ public static List upzipFile(String zipPath, String descDir) { */ @SuppressWarnings("rawtypes") public static List upzipFile(File zipFile, String descDir) { + try { + return upzipFile(zipFile, descDir, new UnzipContext(), 0); + } catch (IOException e) { + throw new TaierDefineException(String.format("Unzip exception : %s", e.getMessage()), e); + } + } + + @SuppressWarnings("rawtypes") + private static List upzipFile(File zipFile, String descDir, UnzipContext context, int depth) throws IOException { + if (depth > MAX_ZIP_RECURSION_DEPTH) { + throw new IOException(String.format("zip recursion depth exceeds limit: %s", MAX_ZIP_RECURSION_DEPTH)); + } List _list = new ArrayList<>(); + File baseDir = new File(descDir); + String basePath = getCanonicalDirPath(baseDir); ZipFile _zipFile = null; - OutputStream _out = null; - InputStream _in = null; try { _zipFile = new ZipFile(zipFile, "GBK"); for (Enumeration entries = _zipFile.getEntries(); entries.hasMoreElements(); ) { org.apache.tools.zip.ZipEntry entry = (org.apache.tools.zip.ZipEntry) entries.nextElement(); - File _file = new File(descDir + File.separator + entry.getName()); + context.addEntry(entry.getName()); + File _file = resolveZipEntryFile(baseDir, basePath, entry.getName()); if (_file.isHidden()) { continue; } if (entry.isDirectory()) { - _file.mkdirs(); + makeDirs(_file); } else { File _parent = _file.getParentFile(); - if (!_parent.exists()) { - _parent.mkdirs(); - } - _in = _zipFile.getInputStream(entry); - _out = new FileOutputStream(_file); + makeDirs(_parent); byte[] buffer = new byte[4]; - int length = _in.read(buffer, 0, 4); - int len = 0; - _out.write(buffer); - while ((len = _in.read(_byte)) > 0) { - _out.write(_byte, 0, len); + byte[] fileBuffer = new byte[1024]; + int length; + try (InputStream _in = _zipFile.getInputStream(entry); + OutputStream _out = new FileOutputStream(_file)) { + length = _in.read(buffer, 0, 4); + long written = 0L; + if (length > 0) { + _out.write(buffer, 0, length); + written += length; + context.addUncompressedSize(length); + validateEntrySize(entry, written); + } + int len = 0; + while ((len = _in.read(fileBuffer)) > 0) { + _out.write(fileBuffer, 0, len); + written += len; + context.addUncompressedSize(len); + validateEntrySize(entry, written); + } + _out.flush(); } - _out.flush(); if (length == 4 && (Arrays.equals(ZIP_HEADER_1, buffer) || Arrays.equals(ZIP_HEADER_2, buffer))) { - _list.addAll(upzipFile(_file, _file.getPath() + "tmp")); + _list.addAll(upzipFile(_file, _file.getPath() + "tmp", context, depth + 1)); } else { _list.add(_file); } } } - } catch (IOException e) { } finally { - if (_out != null) { - try { - _out.close(); - } catch (IOException e) { - } - } - if (_in != null) { - try { - _in.close(); - } catch (IOException e) { - } - } if (_zipFile != null) { - try { - _zipFile.close(); - } catch (IOException e) { - } + _zipFile.close(); } } return _list; } + private static File resolveZipEntryFile(File baseDir, String basePath, String entryName) throws IOException { + File targetFile = new File(baseDir, entryName); + String targetPath = targetFile.getCanonicalPath(); + if (!targetPath.equals(basePath) && !targetPath.startsWith(basePath + File.separator)) { + throw new IOException(String.format("zip entry is outside of target dir: %s", entryName)); + } + return targetFile; + } + + private static String getCanonicalDirPath(File dir) throws IOException { + makeDirs(dir); + return dir.getCanonicalPath(); + } + + private static void makeDirs(File dir) throws IOException { + if (dir != null && !dir.exists() && !dir.mkdirs()) { + throw new IOException(String.format("failed to create directory: %s", dir)); + } + } + + private static void validateEntrySize(org.apache.tools.zip.ZipEntry entry, long written) throws IOException { + if (written > MAX_ZIP_ENTRY_UNCOMPRESSED_SIZE) { + throw new IOException(String.format("zip entry size exceeds limit: %s", entry.getName())); + } + long compressedSize = entry.getCompressedSize(); + if (compressedSize > 0 && written > compressedSize * MAX_ZIP_COMPRESSION_RATIO) { + throw new IOException(String.format("zip entry compression ratio exceeds limit: %s", entry.getName())); + } + } + + private static class UnzipContext { + private int entryCount; + private long totalUncompressedSize; + + private void addEntry(String entryName) throws IOException { + entryCount++; + if (entryCount > MAX_ZIP_ENTRY_COUNT) { + throw new IOException(String.format("zip entry count exceeds limit: %s", entryName)); + } + } + + private void addUncompressedSize(long size) throws IOException { + totalUncompressedSize += size; + if (totalUncompressedSize > MAX_ZIP_TOTAL_UNCOMPRESSED_SIZE) { + throw new IOException("zip total uncompressed size exceeds limit"); + } + } + } + /** * 对临时生成的文件夹和文件夹下的文件进行删除 */ diff --git a/taier-common/src/test/java/com/dtstack/taier/common/util/ZipUtilTest.java b/taier-common/src/test/java/com/dtstack/taier/common/util/ZipUtilTest.java new file mode 100644 index 0000000000..aa9af38047 --- /dev/null +++ b/taier-common/src/test/java/com/dtstack/taier/common/util/ZipUtilTest.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.dtstack.taier.common.util; + +import com.dtstack.taier.common.exception.TaierDefineException; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class ZipUtilTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testUpzipFile() throws Exception { + File zipFile = temporaryFolder.newFile("normal.zip"); + writeZip(zipFile, new ZipItem("conf/core-site.xml", "content")); + File targetDir = temporaryFolder.newFolder("normal"); + + List files = ZipUtil.upzipFile(zipFile, targetDir.getAbsolutePath()); + + Assert.assertEquals(1, files.size()); + File extractedFile = new File(targetDir, "conf/core-site.xml"); + Assert.assertTrue(extractedFile.isFile()); + Assert.assertEquals("content", new String(Files.readAllBytes(extractedFile.toPath()), StandardCharsets.UTF_8)); + } + + @Test + public void testRejectZipSlipEntry() throws Exception { + File zipFile = temporaryFolder.newFile("slip.zip"); + writeZip(zipFile, new ZipItem("../evil.txt", "evil")); + File targetDir = temporaryFolder.newFolder("slip"); + File escapedFile = new File(targetDir.getParentFile(), "evil.txt"); + + try { + ZipUtil.upzipFile(zipFile, targetDir.getAbsolutePath()); + Assert.fail("Zip Slip entry should be rejected"); + } catch (TaierDefineException e) { + Assert.assertTrue(e.getMessage().contains("outside of target dir")); + } + Assert.assertFalse(escapedFile.exists()); + } + + @Test + public void testRejectTooManyEntries() throws Exception { + File zipFile = temporaryFolder.newFile("too-many.zip"); + writeZip(zipFile, buildZipItems(1001)); + File targetDir = temporaryFolder.newFolder("too-many"); + + try { + ZipUtil.upzipFile(zipFile, targetDir.getAbsolutePath()); + Assert.fail("Zip with too many entries should be rejected"); + } catch (TaierDefineException e) { + Assert.assertTrue(e.getMessage().contains("entry count exceeds limit")); + } + } + + @Test + public void testRejectRecursiveZipBomb() throws Exception { + File zipFile = temporaryFolder.newFile("nested.zip"); + writeNestedZip(zipFile, 4); + File targetDir = temporaryFolder.newFolder("nested"); + + try { + ZipUtil.upzipFile(zipFile, targetDir.getAbsolutePath()); + Assert.fail("Recursive zip should be rejected"); + } catch (TaierDefineException e) { + Assert.assertTrue(e.getMessage().contains("recursion depth exceeds limit")); + } + } + + private static ZipItem[] buildZipItems(int count) { + ZipItem[] items = new ZipItem[count]; + for (int i = 0; i < count; i++) { + items[i] = new ZipItem("file-" + i + ".txt", "a"); + } + return items; + } + + private static void writeNestedZip(File zipFile, int depth) throws IOException { + if (depth == 0) { + writeZip(zipFile, new ZipItem("leaf.txt", "leaf")); + return; + } + File innerZip = File.createTempFile("inner", ".zip", zipFile.getParentFile()); + writeNestedZip(innerZip, depth - 1); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFile))) { + zipOutputStream.putNextEntry(new ZipEntry("inner-" + depth + ".zip")); + Files.copy(innerZip.toPath(), zipOutputStream); + zipOutputStream.closeEntry(); + } + Files.delete(innerZip.toPath()); + } + + private static void writeZip(File zipFile, ZipItem... items) throws IOException { + try (ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFile))) { + for (ZipItem item : items) { + zipOutputStream.putNextEntry(new ZipEntry(item.name)); + zipOutputStream.write(item.content.getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + } + } + } + + private static class ZipItem { + private final String name; + private final String content; + + private ZipItem(String name, String content) { + this.name = name; + this.content = content; + } + } +} diff --git a/taier-datasource/taier-datasource-plugin/taier-datasource-plugin-common/src/main/java/com/dtstack/taier/datasource/plugin/common/utils/ZipUtil.java b/taier-datasource/taier-datasource-plugin/taier-datasource-plugin-common/src/main/java/com/dtstack/taier/datasource/plugin/common/utils/ZipUtil.java index 487a3b9d75..202f62a531 100644 --- a/taier-datasource/taier-datasource-plugin/taier-datasource-plugin-common/src/main/java/com/dtstack/taier/datasource/plugin/common/utils/ZipUtil.java +++ b/taier-datasource/taier-datasource-plugin/taier-datasource-plugin-common/src/main/java/com/dtstack/taier/datasource/plugin/common/utils/ZipUtil.java @@ -48,6 +48,11 @@ public class ZipUtil { */ private static byte[] byte_simple = new byte[1024]; + private static final int MAX_ZIP_ENTRY_COUNT = 1000; + private static final long MAX_ZIP_TOTAL_UNCOMPRESSED_SIZE = 100L * 1024 * 1024; + private static final long MAX_ZIP_ENTRY_UNCOMPRESSED_SIZE = 50L * 1024 * 1024; + private static final long MAX_ZIP_COMPRESSION_RATIO = 100L; + /** * 压缩文件或路径 * @@ -80,30 +85,51 @@ public static void zipFile(String zipLocation, String sourceLocation) { */ public static List unzipFile(String zipLocation, String targetLocation) { List files = new ArrayList<>(); + int entryCount = 0; + long totalUncompressedSize = 0L; try { + File baseDir = new File(targetLocation); + String basePath = getCanonicalDirPath(baseDir); + ZipFile zipFile = null; // 构建 ZIP 文件并遍历 - ZipFile zipFile = new ZipFile(zipLocation, "GBK"); - for (Enumeration entries = zipFile.getEntries(); entries.hasMoreElements(); ) { - ZipEntry entry = (ZipEntry) entries.nextElement(); - // 设置目标地址 - File singleFile = new File(targetLocation + File.separator + entry.getName()); - // 如果压缩文件是文件夹则创建 - if (entry.isDirectory()) { - singleFile.mkdirs(); - } else { - File parentFile = singleFile.getParentFile(); - if (!parentFile.exists()) { - parentFile.mkdirs(); + try { + zipFile = new ZipFile(zipLocation, "GBK"); + for (Enumeration entries = zipFile.getEntries(); entries.hasMoreElements(); ) { + ZipEntry entry = (ZipEntry) entries.nextElement(); + entryCount++; + if (entryCount > MAX_ZIP_ENTRY_COUNT) { + throw new SourceException(String.format("Zip entry count exceeds limit: %s", entry.getName())); } - try (InputStream inputStream = zipFile.getInputStream(entry);) { - try (OutputStream outputStream = new FileOutputStream(singleFile);) { - int len = 0; - while ((len = inputStream.read(byte_simple)) > 0) { - outputStream.write(byte_simple, 0, len); + // 设置目标地址 + File singleFile = resolveZipEntryFile(baseDir, basePath, entry.getName()); + // 如果压缩文件是文件夹则创建 + if (entry.isDirectory()) { + makeDirs(singleFile); + } else { + File parentFile = singleFile.getParentFile(); + makeDirs(parentFile); + try (InputStream inputStream = zipFile.getInputStream(entry);) { + try (OutputStream outputStream = new FileOutputStream(singleFile);) { + int len = 0; + byte[] fileBuffer = new byte[1024]; + long entryUncompressedSize = 0L; + while ((len = inputStream.read(fileBuffer)) > 0) { + outputStream.write(fileBuffer, 0, len); + entryUncompressedSize += len; + totalUncompressedSize += len; + validateEntrySize(entry, entryUncompressedSize); + if (totalUncompressedSize > MAX_ZIP_TOTAL_UNCOMPRESSED_SIZE) { + throw new SourceException("Zip total uncompressed size exceeds limit"); + } + } } } + files.add(singleFile); } - files.add(singleFile); + } + } finally { + if (zipFile != null) { + zipFile.close(); } } } catch (IOException e) { @@ -112,6 +138,36 @@ public static List unzipFile(String zipLocation, String targetLocation) { return files; } + private static File resolveZipEntryFile(File baseDir, String basePath, String entryName) throws IOException { + File targetFile = new File(baseDir, entryName); + String targetPath = targetFile.getCanonicalPath(); + if (!targetPath.equals(basePath) && !targetPath.startsWith(basePath + File.separator)) { + throw new SourceException(String.format("Zip entry is outside of target dir: %s", entryName)); + } + return targetFile; + } + + private static String getCanonicalDirPath(File dir) throws IOException { + makeDirs(dir); + return dir.getCanonicalPath(); + } + + private static void makeDirs(File dir) throws IOException { + if (dir != null && !dir.exists() && !dir.mkdirs()) { + throw new IOException(String.format("Failed to create directory: %s", dir)); + } + } + + private static void validateEntrySize(ZipEntry entry, long entryUncompressedSize) { + if (entryUncompressedSize > MAX_ZIP_ENTRY_UNCOMPRESSED_SIZE) { + throw new SourceException(String.format("Zip entry size exceeds limit: %s", entry.getName())); + } + long compressedSize = entry.getCompressedSize(); + if (compressedSize > 0 && entryUncompressedSize > compressedSize * MAX_ZIP_COMPRESSION_RATIO) { + throw new SourceException(String.format("Zip entry compression ratio exceeds limit: %s", entry.getName())); + } + } + /** * @param zipLocation 压缩的目的地址 * @param zipOut ZIP 输出流