From ecde50834641eafc30e2a8460a6bb3546336cde9 Mon Sep 17 00:00:00 2001 From: Looly Date: Wed, 14 Dec 2022 16:29:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DZIP=20bomb=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 +- README-EN.md | 4 +- README.md | 4 +- .../cn/hutool/core/compress/ZipReader.java | 29 +++++++- .../cn/hutool/core/io/LimitedInputStream.java | 63 +++++++++++++++++ .../java/cn/hutool/core/util/ZipUtil.java | 8 ++- .../java/cn/hutool/core/util/ZipUtilTest.java | 70 +++++++++---------- 7 files changed, 136 insertions(+), 45 deletions(-) create mode 100755 hutool-core/src/main/java/cn/hutool/core/io/LimitedInputStream.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a1661432f..09bddf320 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ------------------------------------------------------------------------------------------------------------- -# 5.8.11.M1 (2022-12-11) +# 5.8.11.M1 (2022-12-14) ### 🐣新特性 * 【core 】 CharUtil.isBlankChar增加\u180e(pr#2738@Github) @@ -20,6 +20,7 @@ * 【json 】 修复JSON解析栈溢出部分问题(issue#2746@Github) * 【json 】 修复getMultistageReverseProxyIp未去除空格问题(issue#I64P9J@Gitee) * 【db 】 修复NamedSql中in没有判断大小写问题(issue#2792@Github) +* 【core 】 修复ZIP bomb漏洞(issue#2797@Github) ------------------------------------------------------------------------------------------------------------- diff --git a/README-EN.md b/README-EN.md index 4661fbece..62d3e6089 100755 --- a/README-EN.md +++ b/README-EN.md @@ -40,8 +40,8 @@

- - + +

------------------------------------------------------------------------------- diff --git a/README.md b/README.md index a43175625..a7e03e06c 100755 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@

- - + +

------------------------------------------------------------------------------- diff --git a/hutool-core/src/main/java/cn/hutool/core/compress/ZipReader.java b/hutool-core/src/main/java/cn/hutool/core/compress/ZipReader.java index 9899e95aa..e9d5d9e9b 100755 --- a/hutool-core/src/main/java/cn/hutool/core/compress/ZipReader.java +++ b/hutool-core/src/main/java/cn/hutool/core/compress/ZipReader.java @@ -1,5 +1,6 @@ package cn.hutool.core.compress; +import cn.hutool.core.exceptions.UtilException; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IoUtil; @@ -26,6 +27,9 @@ import java.util.zip.ZipInputStream; */ public class ZipReader implements Closeable { + // size of uncompressed zip entry shouldn't be bigger of compressed in MAX_SIZE_DIFF times + private static final int MAX_SIZE_DIFF = 100; + private ZipFile zipFile; private ZipInputStream in; @@ -203,7 +207,7 @@ public class ZipReader implements Closeable { private void readFromZipFile(Consumer consumer) { final Enumeration em = zipFile.entries(); while (em.hasMoreElements()) { - consumer.accept(em.nextElement()); + consumer.accept(checkZipBomb(em.nextElement())); } } @@ -217,10 +221,31 @@ public class ZipReader implements Closeable { try { ZipEntry zipEntry; while (null != (zipEntry = in.getNextEntry())) { - consumer.accept(zipEntry); + consumer.accept(checkZipBomb(zipEntry)); } } catch (IOException e) { throw new IORuntimeException(e); } } + + /** + * 检查Zip bomb漏洞 + * + * @param entry {@link ZipEntry} + * @return 检查后的{@link ZipEntry} + */ + private static ZipEntry checkZipBomb(ZipEntry entry) { + if (null == entry) { + return null; + } + final long compressedSize = entry.getCompressedSize(); + final long uncompressedSize = entry.getSize(); + if (compressedSize < 0 || uncompressedSize < 0 || + // 默认压缩比例是100倍,一旦发现压缩率超过这个阈值,被认为是Zip bomb + compressedSize * MAX_SIZE_DIFF < uncompressedSize) { + throw new UtilException("Zip bomb attack detected, invalid sizes: compressed {}, uncompressed {}, name {}", + compressedSize, uncompressedSize, entry.getName()); + } + return entry; + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/io/LimitedInputStream.java b/hutool-core/src/main/java/cn/hutool/core/io/LimitedInputStream.java new file mode 100755 index 000000000..dfb51a090 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/LimitedInputStream.java @@ -0,0 +1,63 @@ +package cn.hutool.core.io; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * 限制读取最大长度的{@link FilterInputStream} 实现
+ * 来自:https://github.com/skylot/jadx/blob/master/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/utils/LimitedInputStream.java + * + * @author jadx + */ +public class LimitedInputStream extends FilterInputStream { + + private final long maxSize; + private long currentPos; + + /** + * 构造 + * @param in {@link InputStream} + * @param maxSize 限制最大读取量,单位byte + */ + public LimitedInputStream(InputStream in, long maxSize) { + super(in); + this.maxSize = maxSize; + } + + @Override + public int read() throws IOException { + final int data = super.read(); + if (data != -1) { + currentPos++; + checkPos(); + } + return data; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + final int count = super.read(b, off, len); + if (count > 0) { + currentPos += count; + checkPos(); + } + return count; + } + + @Override + public long skip(long n) throws IOException { + final long skipped = super.skip(n); + if (skipped != 0) { + currentPos += skipped; + checkPos(); + } + return skipped; + } + + private void checkPos() { + if (currentPos > maxSize) { + throw new IllegalStateException("Read limit exceeded"); + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java index 35ab96187..d44059f60 100755 --- a/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java @@ -11,6 +11,7 @@ import cn.hutool.core.io.FastByteArrayOutputStream; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.LimitedInputStream; import cn.hutool.core.io.file.FileSystemUtil; import cn.hutool.core.io.file.PathUtil; import cn.hutool.core.io.resource.Resource; @@ -69,7 +70,8 @@ public class ZipUtil { } /** - * 获取指定{@link ZipEntry}的流,用于读取这个entry的内容 + * 获取指定{@link ZipEntry}的流,用于读取这个entry的内容
+ * 此处使用{@link LimitedInputStream} 限制最大写出大小,避免ZIP bomb漏洞 * * @param zipFile {@link ZipFile} * @param zipEntry {@link ZipEntry} @@ -78,7 +80,7 @@ public class ZipUtil { */ public static InputStream getStream(ZipFile zipFile, ZipEntry zipEntry) { try { - return zipFile.getInputStream(zipEntry); + return new LimitedInputStream(zipFile.getInputStream(zipEntry), zipEntry.getSize()); } catch (IOException e) { throw new IORuntimeException(e); } @@ -574,7 +576,7 @@ public class ZipUtil { final Enumeration zipEntries = zipFile.entries(); long zipFileSize = 0L; while (zipEntries.hasMoreElements()) { - ZipEntry zipEntry = zipEntries.nextElement(); + final ZipEntry zipEntry = zipEntries.nextElement(); zipFileSize += zipEntry.getSize(); if (zipFileSize > limit) { throw new IllegalArgumentException("The file size exceeds the limit"); diff --git a/hutool-core/src/test/java/cn/hutool/core/util/ZipUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/ZipUtilTest.java index 1a1ef934b..c9428ab38 100644 --- a/hutool-core/src/test/java/cn/hutool/core/util/ZipUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/util/ZipUtilTest.java @@ -29,11 +29,11 @@ public class ZipUtilTest { @Test public void appendTest() throws IOException { - File appendFile = FileUtil.file("test-zip/addFile.txt"); - File zipFile = FileUtil.file("test-zip/test.zip"); + final File appendFile = FileUtil.file("test-zip/addFile.txt"); + final File zipFile = FileUtil.file("test-zip/test.zip"); // 用于测试完成后将被测试文件恢复 - File tempZipFile = FileUtil.createTempFile(FileUtil.file("test-zip")); + final File tempZipFile = FileUtil.createTempFile(FileUtil.file("test-zip")); tempZipFile.deleteOnExit(); FileUtil.copy(zipFile, tempZipFile, true); @@ -49,7 +49,7 @@ public class ZipUtilTest { // test dir add beforeNames = zipEntryNames(tempZipFile); - File addDirFile = FileUtil.file("test-zip/test-add"); + final File addDirFile = FileUtil.file("test-zip/test-add"); ZipUtil.append(tempZipFile.toPath(), addDirFile.toPath()); afterNames = zipEntryNames(tempZipFile); @@ -68,9 +68,9 @@ public class ZipUtilTest { * @param zipFile 待测试的zip文件 * @return zip文件中一级目录下的所有文件/文件夹名 */ - private List zipEntryNames(File zipFile) { - List fileNames = new ArrayList<>(); - ZipReader reader = ZipReader.of(zipFile, CharsetUtil.CHARSET_UTF_8); + private List zipEntryNames(final File zipFile) { + final List fileNames = new ArrayList<>(); + final ZipReader reader = ZipReader.of(zipFile, CharsetUtil.CHARSET_UTF_8); reader.read(zipEntry -> fileNames.add(zipEntry.getName())); reader.close(); return fileNames; @@ -85,21 +85,21 @@ public class ZipUtilTest { @Test @Ignore public void unzipTest() { - File unzip = ZipUtil.unzip("f:/test/apache-maven-3.6.2.zip", "f:\\test"); + final File unzip = ZipUtil.unzip("d:/test/hutool.zip", "d:\\test", CharsetUtil.CHARSET_GBK); Console.log(unzip); } @Test @Ignore public void unzipTest2() { - File unzip = ZipUtil.unzip("f:/test/各种资源.zip", "f:/test/各种资源", CharsetUtil.CHARSET_GBK); + final File unzip = ZipUtil.unzip("f:/test/各种资源.zip", "f:/test/各种资源", CharsetUtil.CHARSET_GBK); Console.log(unzip); } @Test @Ignore public void unzipFromStreamTest() { - File unzip = ZipUtil.unzip(FileUtil.getInputStream("e:/test/hutool-core-5.1.0.jar"), FileUtil.file("e:/test/"), CharsetUtil.CHARSET_UTF_8); + final File unzip = ZipUtil.unzip(FileUtil.getInputStream("e:/test/hutool-core-5.1.0.jar"), FileUtil.file("e:/test/"), CharsetUtil.CHARSET_UTF_8); Console.log(unzip); } @@ -112,40 +112,40 @@ public class ZipUtilTest { @Test @Ignore public void unzipFileBytesTest() { - byte[] fileBytes = ZipUtil.unzipFileBytes(FileUtil.file("e:/02 电力相关设备及服务2-241-.zip"), CharsetUtil.CHARSET_GBK, "images/CE-EP-HY-MH01-ES-0001.jpg"); + final byte[] fileBytes = ZipUtil.unzipFileBytes(FileUtil.file("e:/02 电力相关设备及服务2-241-.zip"), CharsetUtil.CHARSET_GBK, "images/CE-EP-HY-MH01-ES-0001.jpg"); Assert.assertNotNull(fileBytes); } @Test public void gzipTest() { - String data = "我是一个需要压缩的很长很长的字符串"; - byte[] bytes = StrUtil.utf8Bytes(data); - byte[] gzip = ZipUtil.gzip(bytes); + final String data = "我是一个需要压缩的很长很长的字符串"; + final byte[] bytes = StrUtil.utf8Bytes(data); + final byte[] gzip = ZipUtil.gzip(bytes); //保证gzip长度正常 Assert.assertEquals(68, gzip.length); - byte[] unGzip = ZipUtil.unGzip(gzip); + final byte[] unGzip = ZipUtil.unGzip(gzip); //保证正常还原 Assert.assertEquals(data, StrUtil.utf8Str(unGzip)); } @Test public void zlibTest() { - String data = "我是一个需要压缩的很长很长的字符串"; - byte[] bytes = StrUtil.utf8Bytes(data); + final String data = "我是一个需要压缩的很长很长的字符串"; + final byte[] bytes = StrUtil.utf8Bytes(data); byte[] gzip = ZipUtil.zlib(bytes, 0); //保证zlib长度正常 Assert.assertEquals(62, gzip.length); - byte[] unGzip = ZipUtil.unZlib(gzip); + final byte[] unGzip = ZipUtil.unZlib(gzip); //保证正常还原 Assert.assertEquals(data, StrUtil.utf8Str(unGzip)); gzip = ZipUtil.zlib(bytes, 9); //保证zlib长度正常 Assert.assertEquals(56, gzip.length); - byte[] unGzip2 = ZipUtil.unZlib(gzip); + final byte[] unGzip2 = ZipUtil.unZlib(gzip); //保证正常还原 Assert.assertEquals(data, StrUtil.utf8Str(unGzip2)); } @@ -154,13 +154,13 @@ public class ZipUtilTest { @Ignore public void zipStreamTest(){ //https://github.com/dromara/hutool/issues/944 - String dir = "d:/test"; - String zip = "d:/test.zip"; + final String dir = "d:/test"; + final String zip = "d:/test.zip"; //noinspection IOStreamConstructor - try (OutputStream out = new FileOutputStream(zip)){ + try (final OutputStream out = new FileOutputStream(zip)){ //实际应用中, out 为 HttpServletResponse.getOutputStream ZipUtil.zip(out, Charset.defaultCharset(), false, null, new File(dir)); - } catch (IOException e) { + } catch (final IOException e) { throw new IORuntimeException(e); } } @@ -169,11 +169,11 @@ public class ZipUtilTest { @Ignore public void zipStreamTest2(){ // https://github.com/dromara/hutool/issues/944 - String file1 = "d:/test/a.txt"; - String file2 = "d:/test/a.txt"; - String file3 = "d:/test/asn1.key"; + final String file1 = "d:/test/a.txt"; + final String file2 = "d:/test/a.txt"; + final String file3 = "d:/test/asn1.key"; - String zip = "d:/test/test2.zip"; + final String zip = "d:/test/test2.zip"; //实际应用中, out 为 HttpServletResponse.getOutputStream ZipUtil.zip(FileUtil.getOutputStream(zip), Charset.defaultCharset(), false, null, new File(file1), @@ -185,8 +185,8 @@ public class ZipUtilTest { @Test @Ignore public void zipToStreamTest(){ - String zip = "d:/test/testToStream.zip"; - OutputStream out = FileUtil.getOutputStream(zip); + final String zip = "d:/test/testToStream.zip"; + final OutputStream out = FileUtil.getOutputStream(zip); ZipUtil.zip(out, new String[]{"sm1_alias.txt"}, new InputStream[]{FileUtil.getInputStream("d:/test/sm4_1.txt")}); } @@ -194,7 +194,7 @@ public class ZipUtilTest { @Test @Ignore public void zipMultiFileTest(){ - File[] dd={FileUtil.file("d:\\test\\qr_a.jpg") + final File[] dd={FileUtil.file("d:\\test\\qr_a.jpg") ,FileUtil.file("d:\\test\\qr_b.jpg")}; ZipUtil.zip(FileUtil.file("d:\\test\\qr.zip"),false,dd); @@ -203,12 +203,12 @@ public class ZipUtilTest { @Test @Ignore public void sizeUnzipTest() throws IOException { - String zipPath = "e:\\hutool\\demo.zip"; - String outPath = "e:\\hutool\\test"; - ZipFile zipFile = new ZipFile(zipPath, Charset.forName("GBK")); - File file = new File(outPath); + final String zipPath = "e:\\hutool\\demo.zip"; + final String outPath = "e:\\hutool\\test"; + final ZipFile zipFile = new ZipFile(zipPath, Charset.forName("GBK")); + final File file = new File(outPath); // 限制解压文件大小为637KB - long size = 637*1024L; + final long size = 637*1024L; // 限制解压文件大小为636KB // long size = 636*1024L;