Skip to content

Commit 0bd48ec

Browse files
authored
[issue_1199][cve] fix ZipUtil Zip Slip & Zip Bomb Vulnerability Report #1199 (#1200)
1 parent a3c6ae3 commit 0bd48ec

3 files changed

Lines changed: 311 additions & 51 deletions

File tree

  • taier-common/src
  • taier-datasource/taier-datasource-plugin/taier-datasource-plugin-common/src/main/java/com/dtstack/taier/datasource/plugin/common/utils

taier-common/src/main/java/com/dtstack/taier/common/util/ZipUtil.java

Lines changed: 96 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
package com.dtstack.taier.common.util;
2020

21+
import com.dtstack.taier.common.exception.TaierDefineException;
2122
import org.apache.tools.zip.ZipFile;
2223
import org.slf4j.Logger;
2324
import org.slf4j.LoggerFactory;
@@ -54,6 +55,12 @@ public class ZipUtil {
5455

5556
private static byte[] _byte = new byte[1024];
5657

58+
private static final int MAX_ZIP_ENTRY_COUNT = 1000;
59+
private static final int MAX_ZIP_RECURSION_DEPTH = 3;
60+
private static final long MAX_ZIP_TOTAL_UNCOMPRESSED_SIZE = 100L * 1024 * 1024;
61+
private static final long MAX_ZIP_ENTRY_UNCOMPRESSED_SIZE = 50L * 1024 * 1024;
62+
private static final long MAX_ZIP_COMPRESSION_RATIO = 100L;
63+
5764
public static byte[] compress(byte[] rowData) {
5865
byte[] backData = null;
5966
ZipOutputStream zip = null;
@@ -224,68 +231,124 @@ public static List<File> upzipFile(String zipPath, String descDir) {
224231
*/
225232
@SuppressWarnings("rawtypes")
226233
public static List<File> upzipFile(File zipFile, String descDir) {
234+
try {
235+
return upzipFile(zipFile, descDir, new UnzipContext(), 0);
236+
} catch (IOException e) {
237+
throw new TaierDefineException(String.format("Unzip exception : %s", e.getMessage()), e);
238+
}
239+
}
240+
241+
@SuppressWarnings("rawtypes")
242+
private static List<File> upzipFile(File zipFile, String descDir, UnzipContext context, int depth) throws IOException {
243+
if (depth > MAX_ZIP_RECURSION_DEPTH) {
244+
throw new IOException(String.format("zip recursion depth exceeds limit: %s", MAX_ZIP_RECURSION_DEPTH));
245+
}
227246
List<File> _list = new ArrayList<>();
247+
File baseDir = new File(descDir);
248+
String basePath = getCanonicalDirPath(baseDir);
228249
ZipFile _zipFile = null;
229-
OutputStream _out = null;
230-
InputStream _in = null;
231250
try {
232251
_zipFile = new ZipFile(zipFile, "GBK");
233252
for (Enumeration entries = _zipFile.getEntries(); entries.hasMoreElements(); ) {
234253
org.apache.tools.zip.ZipEntry entry = (org.apache.tools.zip.ZipEntry) entries.nextElement();
235-
File _file = new File(descDir + File.separator + entry.getName());
254+
context.addEntry(entry.getName());
255+
File _file = resolveZipEntryFile(baseDir, basePath, entry.getName());
236256
if (_file.isHidden()) {
237257
continue;
238258
}
239259
if (entry.isDirectory()) {
240-
_file.mkdirs();
260+
makeDirs(_file);
241261
} else {
242262
File _parent = _file.getParentFile();
243-
if (!_parent.exists()) {
244-
_parent.mkdirs();
245-
}
246-
_in = _zipFile.getInputStream(entry);
247-
_out = new FileOutputStream(_file);
263+
makeDirs(_parent);
248264
byte[] buffer = new byte[4];
249-
int length = _in.read(buffer, 0, 4);
250-
int len = 0;
251-
_out.write(buffer);
252-
while ((len = _in.read(_byte)) > 0) {
253-
_out.write(_byte, 0, len);
265+
byte[] fileBuffer = new byte[1024];
266+
int length;
267+
try (InputStream _in = _zipFile.getInputStream(entry);
268+
OutputStream _out = new FileOutputStream(_file)) {
269+
length = _in.read(buffer, 0, 4);
270+
long written = 0L;
271+
if (length > 0) {
272+
_out.write(buffer, 0, length);
273+
written += length;
274+
context.addUncompressedSize(length);
275+
validateEntrySize(entry, written);
276+
}
277+
int len = 0;
278+
while ((len = _in.read(fileBuffer)) > 0) {
279+
_out.write(fileBuffer, 0, len);
280+
written += len;
281+
context.addUncompressedSize(len);
282+
validateEntrySize(entry, written);
283+
}
284+
_out.flush();
254285
}
255-
_out.flush();
256286

257287
if (length == 4 && (Arrays.equals(ZIP_HEADER_1, buffer) || Arrays.equals(ZIP_HEADER_2, buffer))) {
258-
_list.addAll(upzipFile(_file, _file.getPath() + "tmp"));
288+
_list.addAll(upzipFile(_file, _file.getPath() + "tmp", context, depth + 1));
259289
} else {
260290
_list.add(_file);
261291
}
262292

263293
}
264294
}
265-
} catch (IOException e) {
266295
} finally {
267-
if (_out != null) {
268-
try {
269-
_out.close();
270-
} catch (IOException e) {
271-
}
272-
}
273-
if (_in != null) {
274-
try {
275-
_in.close();
276-
} catch (IOException e) {
277-
}
278-
}
279296
if (_zipFile != null) {
280-
try {
281-
_zipFile.close();
282-
} catch (IOException e) {
283-
}
297+
_zipFile.close();
284298
}
285299
}
286300
return _list;
287301
}
288302

303+
private static File resolveZipEntryFile(File baseDir, String basePath, String entryName) throws IOException {
304+
File targetFile = new File(baseDir, entryName);
305+
String targetPath = targetFile.getCanonicalPath();
306+
if (!targetPath.equals(basePath) && !targetPath.startsWith(basePath + File.separator)) {
307+
throw new IOException(String.format("zip entry is outside of target dir: %s", entryName));
308+
}
309+
return targetFile;
310+
}
311+
312+
private static String getCanonicalDirPath(File dir) throws IOException {
313+
makeDirs(dir);
314+
return dir.getCanonicalPath();
315+
}
316+
317+
private static void makeDirs(File dir) throws IOException {
318+
if (dir != null && !dir.exists() && !dir.mkdirs()) {
319+
throw new IOException(String.format("failed to create directory: %s", dir));
320+
}
321+
}
322+
323+
private static void validateEntrySize(org.apache.tools.zip.ZipEntry entry, long written) throws IOException {
324+
if (written > MAX_ZIP_ENTRY_UNCOMPRESSED_SIZE) {
325+
throw new IOException(String.format("zip entry size exceeds limit: %s", entry.getName()));
326+
}
327+
long compressedSize = entry.getCompressedSize();
328+
if (compressedSize > 0 && written > compressedSize * MAX_ZIP_COMPRESSION_RATIO) {
329+
throw new IOException(String.format("zip entry compression ratio exceeds limit: %s", entry.getName()));
330+
}
331+
}
332+
333+
private static class UnzipContext {
334+
private int entryCount;
335+
private long totalUncompressedSize;
336+
337+
private void addEntry(String entryName) throws IOException {
338+
entryCount++;
339+
if (entryCount > MAX_ZIP_ENTRY_COUNT) {
340+
throw new IOException(String.format("zip entry count exceeds limit: %s", entryName));
341+
}
342+
}
343+
344+
private void addUncompressedSize(long size) throws IOException {
345+
totalUncompressedSize += size;
346+
if (totalUncompressedSize > MAX_ZIP_TOTAL_UNCOMPRESSED_SIZE) {
347+
throw new IOException("zip total uncompressed size exceeds limit");
348+
}
349+
}
350+
}
351+
289352
/**
290353
* 对临时生成的文件夹和文件夹下的文件进行删除
291354
*/
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package com.dtstack.taier.common.util;
20+
21+
import com.dtstack.taier.common.exception.TaierDefineException;
22+
import org.junit.Assert;
23+
import org.junit.Rule;
24+
import org.junit.Test;
25+
import org.junit.rules.TemporaryFolder;
26+
27+
import java.io.File;
28+
import java.io.FileOutputStream;
29+
import java.io.IOException;
30+
import java.nio.charset.StandardCharsets;
31+
import java.nio.file.Files;
32+
import java.util.List;
33+
import java.util.zip.ZipEntry;
34+
import java.util.zip.ZipOutputStream;
35+
36+
public class ZipUtilTest {
37+
38+
@Rule
39+
public TemporaryFolder temporaryFolder = new TemporaryFolder();
40+
41+
@Test
42+
public void testUpzipFile() throws Exception {
43+
File zipFile = temporaryFolder.newFile("normal.zip");
44+
writeZip(zipFile, new ZipItem("conf/core-site.xml", "content"));
45+
File targetDir = temporaryFolder.newFolder("normal");
46+
47+
List<File> files = ZipUtil.upzipFile(zipFile, targetDir.getAbsolutePath());
48+
49+
Assert.assertEquals(1, files.size());
50+
File extractedFile = new File(targetDir, "conf/core-site.xml");
51+
Assert.assertTrue(extractedFile.isFile());
52+
Assert.assertEquals("content", new String(Files.readAllBytes(extractedFile.toPath()), StandardCharsets.UTF_8));
53+
}
54+
55+
@Test
56+
public void testRejectZipSlipEntry() throws Exception {
57+
File zipFile = temporaryFolder.newFile("slip.zip");
58+
writeZip(zipFile, new ZipItem("../evil.txt", "evil"));
59+
File targetDir = temporaryFolder.newFolder("slip");
60+
File escapedFile = new File(targetDir.getParentFile(), "evil.txt");
61+
62+
try {
63+
ZipUtil.upzipFile(zipFile, targetDir.getAbsolutePath());
64+
Assert.fail("Zip Slip entry should be rejected");
65+
} catch (TaierDefineException e) {
66+
Assert.assertTrue(e.getMessage().contains("outside of target dir"));
67+
}
68+
Assert.assertFalse(escapedFile.exists());
69+
}
70+
71+
@Test
72+
public void testRejectTooManyEntries() throws Exception {
73+
File zipFile = temporaryFolder.newFile("too-many.zip");
74+
writeZip(zipFile, buildZipItems(1001));
75+
File targetDir = temporaryFolder.newFolder("too-many");
76+
77+
try {
78+
ZipUtil.upzipFile(zipFile, targetDir.getAbsolutePath());
79+
Assert.fail("Zip with too many entries should be rejected");
80+
} catch (TaierDefineException e) {
81+
Assert.assertTrue(e.getMessage().contains("entry count exceeds limit"));
82+
}
83+
}
84+
85+
@Test
86+
public void testRejectRecursiveZipBomb() throws Exception {
87+
File zipFile = temporaryFolder.newFile("nested.zip");
88+
writeNestedZip(zipFile, 4);
89+
File targetDir = temporaryFolder.newFolder("nested");
90+
91+
try {
92+
ZipUtil.upzipFile(zipFile, targetDir.getAbsolutePath());
93+
Assert.fail("Recursive zip should be rejected");
94+
} catch (TaierDefineException e) {
95+
Assert.assertTrue(e.getMessage().contains("recursion depth exceeds limit"));
96+
}
97+
}
98+
99+
private static ZipItem[] buildZipItems(int count) {
100+
ZipItem[] items = new ZipItem[count];
101+
for (int i = 0; i < count; i++) {
102+
items[i] = new ZipItem("file-" + i + ".txt", "a");
103+
}
104+
return items;
105+
}
106+
107+
private static void writeNestedZip(File zipFile, int depth) throws IOException {
108+
if (depth == 0) {
109+
writeZip(zipFile, new ZipItem("leaf.txt", "leaf"));
110+
return;
111+
}
112+
File innerZip = File.createTempFile("inner", ".zip", zipFile.getParentFile());
113+
writeNestedZip(innerZip, depth - 1);
114+
try (ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFile))) {
115+
zipOutputStream.putNextEntry(new ZipEntry("inner-" + depth + ".zip"));
116+
Files.copy(innerZip.toPath(), zipOutputStream);
117+
zipOutputStream.closeEntry();
118+
}
119+
Files.delete(innerZip.toPath());
120+
}
121+
122+
private static void writeZip(File zipFile, ZipItem... items) throws IOException {
123+
try (ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFile))) {
124+
for (ZipItem item : items) {
125+
zipOutputStream.putNextEntry(new ZipEntry(item.name));
126+
zipOutputStream.write(item.content.getBytes(StandardCharsets.UTF_8));
127+
zipOutputStream.closeEntry();
128+
}
129+
}
130+
}
131+
132+
private static class ZipItem {
133+
private final String name;
134+
private final String content;
135+
136+
private ZipItem(String name, String content) {
137+
this.name = name;
138+
this.content = content;
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)