修复ZIP bomb漏洞

This commit is contained in:
Looly 2022-12-14 16:29:39 +08:00
parent edcbbb8396
commit ecde508346
7 changed files with 136 additions and 45 deletions

View File

@ -3,7 +3,7 @@
------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------
# 5.8.11.M1 (2022-12-11) # 5.8.11.M1 (2022-12-14)
### 🐣新特性 ### 🐣新特性
* 【core 】 CharUtil.isBlankChar增加\u180epr#2738@Github * 【core 】 CharUtil.isBlankChar增加\u180epr#2738@Github
@ -20,6 +20,7 @@
* 【json 】 修复JSON解析栈溢出部分问题issue#2746@Github * 【json 】 修复JSON解析栈溢出部分问题issue#2746@Github
* 【json 】 修复getMultistageReverseProxyIp未去除空格问题issue#I64P9J@Gitee * 【json 】 修复getMultistageReverseProxyIp未去除空格问题issue#I64P9J@Gitee
* 【db 】 修复NamedSql中in没有判断大小写问题issue#2792@Github * 【db 】 修复NamedSql中in没有判断大小写问题issue#2792@Github
* 【core 】 修复ZIP bomb漏洞issue#2797@Github
------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------

View File

@ -40,8 +40,8 @@
<br/> <br/>
<p align="center"> <p align="center">
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=jPq9DsjXs7GUWbXRZU3wygSJyMEy4pqr&jump_from=webapi"> <a href="https://qm.qq.com/cgi-bin/qm/qr?k=QtsqXLkHpLjE99tkre19j6pjPMhSay1a&jump_from=webapi">
<img src="https://img.shields.io/badge/QQ%E7%BE%A4%E2%91%A5-610134978-orange"/></a> <img src="https://img.shields.io/badge/QQ%E7%BE%A4%E2%91%A5-715292493-orange"/></a>
</p> </p>
------------------------------------------------------------------------------- -------------------------------------------------------------------------------

View File

@ -40,8 +40,8 @@
<br/> <br/>
<p align="center"> <p align="center">
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=jPq9DsjXs7GUWbXRZU3wygSJyMEy4pqr&jump_from=webapi"> <a href="https://qm.qq.com/cgi-bin/qm/qr?k=QtsqXLkHpLjE99tkre19j6pjPMhSay1a&jump_from=webapi">
<img src="https://img.shields.io/badge/QQ%E7%BE%A4%E2%91%A5-610134978-orange"/></a> <img src="https://img.shields.io/badge/QQ%E7%BE%A4%E2%91%A5-715292493-orange"/></a>
</p> </p>
------------------------------------------------------------------------------- -------------------------------------------------------------------------------

View File

@ -1,5 +1,6 @@
package cn.hutool.core.compress; package cn.hutool.core.compress;
import cn.hutool.core.exceptions.UtilException;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
@ -26,6 +27,9 @@ import java.util.zip.ZipInputStream;
*/ */
public class ZipReader implements Closeable { 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 ZipFile zipFile;
private ZipInputStream in; private ZipInputStream in;
@ -203,7 +207,7 @@ public class ZipReader implements Closeable {
private void readFromZipFile(Consumer<ZipEntry> consumer) { private void readFromZipFile(Consumer<ZipEntry> consumer) {
final Enumeration<? extends ZipEntry> em = zipFile.entries(); final Enumeration<? extends ZipEntry> em = zipFile.entries();
while (em.hasMoreElements()) { while (em.hasMoreElements()) {
consumer.accept(em.nextElement()); consumer.accept(checkZipBomb(em.nextElement()));
} }
} }
@ -217,10 +221,31 @@ public class ZipReader implements Closeable {
try { try {
ZipEntry zipEntry; ZipEntry zipEntry;
while (null != (zipEntry = in.getNextEntry())) { while (null != (zipEntry = in.getNextEntry())) {
consumer.accept(zipEntry); consumer.accept(checkZipBomb(zipEntry));
} }
} catch (IOException e) { } catch (IOException e) {
throw new IORuntimeException(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;
}
} }

View File

@ -0,0 +1,63 @@
package cn.hutool.core.io;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 限制读取最大长度的{@link FilterInputStream} 实现<br>
* 来自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");
}
}
}

View File

@ -11,6 +11,7 @@ import cn.hutool.core.io.FastByteArrayOutputStream;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil; 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.FileSystemUtil;
import cn.hutool.core.io.file.PathUtil; import cn.hutool.core.io.file.PathUtil;
import cn.hutool.core.io.resource.Resource; import cn.hutool.core.io.resource.Resource;
@ -69,7 +70,8 @@ public class ZipUtil {
} }
/** /**
* 获取指定{@link ZipEntry}的流用于读取这个entry的内容 * 获取指定{@link ZipEntry}的流用于读取这个entry的内容<br>
* 此处使用{@link LimitedInputStream} 限制最大写出大小避免ZIP bomb漏洞
* *
* @param zipFile {@link ZipFile} * @param zipFile {@link ZipFile}
* @param zipEntry {@link ZipEntry} * @param zipEntry {@link ZipEntry}
@ -78,7 +80,7 @@ public class ZipUtil {
*/ */
public static InputStream getStream(ZipFile zipFile, ZipEntry zipEntry) { public static InputStream getStream(ZipFile zipFile, ZipEntry zipEntry) {
try { try {
return zipFile.getInputStream(zipEntry); return new LimitedInputStream(zipFile.getInputStream(zipEntry), zipEntry.getSize());
} catch (IOException e) { } catch (IOException e) {
throw new IORuntimeException(e); throw new IORuntimeException(e);
} }
@ -574,7 +576,7 @@ public class ZipUtil {
final Enumeration<? extends ZipEntry> zipEntries = zipFile.entries(); final Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
long zipFileSize = 0L; long zipFileSize = 0L;
while (zipEntries.hasMoreElements()) { while (zipEntries.hasMoreElements()) {
ZipEntry zipEntry = zipEntries.nextElement(); final ZipEntry zipEntry = zipEntries.nextElement();
zipFileSize += zipEntry.getSize(); zipFileSize += zipEntry.getSize();
if (zipFileSize > limit) { if (zipFileSize > limit) {
throw new IllegalArgumentException("The file size exceeds the limit"); throw new IllegalArgumentException("The file size exceeds the limit");

View File

@ -29,11 +29,11 @@ public class ZipUtilTest {
@Test @Test
public void appendTest() throws IOException { public void appendTest() throws IOException {
File appendFile = FileUtil.file("test-zip/addFile.txt"); final File appendFile = FileUtil.file("test-zip/addFile.txt");
File zipFile = FileUtil.file("test-zip/test.zip"); 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(); tempZipFile.deleteOnExit();
FileUtil.copy(zipFile, tempZipFile, true); FileUtil.copy(zipFile, tempZipFile, true);
@ -49,7 +49,7 @@ public class ZipUtilTest {
// test dir add // test dir add
beforeNames = zipEntryNames(tempZipFile); 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()); ZipUtil.append(tempZipFile.toPath(), addDirFile.toPath());
afterNames = zipEntryNames(tempZipFile); afterNames = zipEntryNames(tempZipFile);
@ -68,9 +68,9 @@ public class ZipUtilTest {
* @param zipFile 待测试的zip文件 * @param zipFile 待测试的zip文件
* @return zip文件中一级目录下的所有文件/文件夹名 * @return zip文件中一级目录下的所有文件/文件夹名
*/ */
private List<String> zipEntryNames(File zipFile) { private List<String> zipEntryNames(final File zipFile) {
List<String> fileNames = new ArrayList<>(); final List<String> fileNames = new ArrayList<>();
ZipReader reader = ZipReader.of(zipFile, CharsetUtil.CHARSET_UTF_8); final ZipReader reader = ZipReader.of(zipFile, CharsetUtil.CHARSET_UTF_8);
reader.read(zipEntry -> fileNames.add(zipEntry.getName())); reader.read(zipEntry -> fileNames.add(zipEntry.getName()));
reader.close(); reader.close();
return fileNames; return fileNames;
@ -85,21 +85,21 @@ public class ZipUtilTest {
@Test @Test
@Ignore @Ignore
public void unzipTest() { 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); Console.log(unzip);
} }
@Test @Test
@Ignore @Ignore
public void unzipTest2() { 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); Console.log(unzip);
} }
@Test @Test
@Ignore @Ignore
public void unzipFromStreamTest() { 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); Console.log(unzip);
} }
@ -112,40 +112,40 @@ public class ZipUtilTest {
@Test @Test
@Ignore @Ignore
public void unzipFileBytesTest() { 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); Assert.assertNotNull(fileBytes);
} }
@Test @Test
public void gzipTest() { public void gzipTest() {
String data = "我是一个需要压缩的很长很长的字符串"; final String data = "我是一个需要压缩的很长很长的字符串";
byte[] bytes = StrUtil.utf8Bytes(data); final byte[] bytes = StrUtil.utf8Bytes(data);
byte[] gzip = ZipUtil.gzip(bytes); final byte[] gzip = ZipUtil.gzip(bytes);
//保证gzip长度正常 //保证gzip长度正常
Assert.assertEquals(68, gzip.length); Assert.assertEquals(68, gzip.length);
byte[] unGzip = ZipUtil.unGzip(gzip); final byte[] unGzip = ZipUtil.unGzip(gzip);
//保证正常还原 //保证正常还原
Assert.assertEquals(data, StrUtil.utf8Str(unGzip)); Assert.assertEquals(data, StrUtil.utf8Str(unGzip));
} }
@Test @Test
public void zlibTest() { public void zlibTest() {
String data = "我是一个需要压缩的很长很长的字符串"; final String data = "我是一个需要压缩的很长很长的字符串";
byte[] bytes = StrUtil.utf8Bytes(data); final byte[] bytes = StrUtil.utf8Bytes(data);
byte[] gzip = ZipUtil.zlib(bytes, 0); byte[] gzip = ZipUtil.zlib(bytes, 0);
//保证zlib长度正常 //保证zlib长度正常
Assert.assertEquals(62, gzip.length); Assert.assertEquals(62, gzip.length);
byte[] unGzip = ZipUtil.unZlib(gzip); final byte[] unGzip = ZipUtil.unZlib(gzip);
//保证正常还原 //保证正常还原
Assert.assertEquals(data, StrUtil.utf8Str(unGzip)); Assert.assertEquals(data, StrUtil.utf8Str(unGzip));
gzip = ZipUtil.zlib(bytes, 9); gzip = ZipUtil.zlib(bytes, 9);
//保证zlib长度正常 //保证zlib长度正常
Assert.assertEquals(56, gzip.length); Assert.assertEquals(56, gzip.length);
byte[] unGzip2 = ZipUtil.unZlib(gzip); final byte[] unGzip2 = ZipUtil.unZlib(gzip);
//保证正常还原 //保证正常还原
Assert.assertEquals(data, StrUtil.utf8Str(unGzip2)); Assert.assertEquals(data, StrUtil.utf8Str(unGzip2));
} }
@ -154,13 +154,13 @@ public class ZipUtilTest {
@Ignore @Ignore
public void zipStreamTest(){ public void zipStreamTest(){
//https://github.com/dromara/hutool/issues/944 //https://github.com/dromara/hutool/issues/944
String dir = "d:/test"; final String dir = "d:/test";
String zip = "d:/test.zip"; final String zip = "d:/test.zip";
//noinspection IOStreamConstructor //noinspection IOStreamConstructor
try (OutputStream out = new FileOutputStream(zip)){ try (final OutputStream out = new FileOutputStream(zip)){
//实际应用中, out HttpServletResponse.getOutputStream //实际应用中, out HttpServletResponse.getOutputStream
ZipUtil.zip(out, Charset.defaultCharset(), false, null, new File(dir)); ZipUtil.zip(out, Charset.defaultCharset(), false, null, new File(dir));
} catch (IOException e) { } catch (final IOException e) {
throw new IORuntimeException(e); throw new IORuntimeException(e);
} }
} }
@ -169,11 +169,11 @@ public class ZipUtilTest {
@Ignore @Ignore
public void zipStreamTest2(){ public void zipStreamTest2(){
// https://github.com/dromara/hutool/issues/944 // https://github.com/dromara/hutool/issues/944
String file1 = "d:/test/a.txt"; final String file1 = "d:/test/a.txt";
String file2 = "d:/test/a.txt"; final String file2 = "d:/test/a.txt";
String file3 = "d:/test/asn1.key"; final String file3 = "d:/test/asn1.key";
String zip = "d:/test/test2.zip"; final String zip = "d:/test/test2.zip";
//实际应用中, out HttpServletResponse.getOutputStream //实际应用中, out HttpServletResponse.getOutputStream
ZipUtil.zip(FileUtil.getOutputStream(zip), Charset.defaultCharset(), false, null, ZipUtil.zip(FileUtil.getOutputStream(zip), Charset.defaultCharset(), false, null,
new File(file1), new File(file1),
@ -185,8 +185,8 @@ public class ZipUtilTest {
@Test @Test
@Ignore @Ignore
public void zipToStreamTest(){ public void zipToStreamTest(){
String zip = "d:/test/testToStream.zip"; final String zip = "d:/test/testToStream.zip";
OutputStream out = FileUtil.getOutputStream(zip); final OutputStream out = FileUtil.getOutputStream(zip);
ZipUtil.zip(out, new String[]{"sm1_alias.txt"}, ZipUtil.zip(out, new String[]{"sm1_alias.txt"},
new InputStream[]{FileUtil.getInputStream("d:/test/sm4_1.txt")}); new InputStream[]{FileUtil.getInputStream("d:/test/sm4_1.txt")});
} }
@ -194,7 +194,7 @@ public class ZipUtilTest {
@Test @Test
@Ignore @Ignore
public void zipMultiFileTest(){ 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")}; ,FileUtil.file("d:\\test\\qr_b.jpg")};
ZipUtil.zip(FileUtil.file("d:\\test\\qr.zip"),false,dd); ZipUtil.zip(FileUtil.file("d:\\test\\qr.zip"),false,dd);
@ -203,12 +203,12 @@ public class ZipUtilTest {
@Test @Test
@Ignore @Ignore
public void sizeUnzipTest() throws IOException { public void sizeUnzipTest() throws IOException {
String zipPath = "e:\\hutool\\demo.zip"; final String zipPath = "e:\\hutool\\demo.zip";
String outPath = "e:\\hutool\\test"; final String outPath = "e:\\hutool\\test";
ZipFile zipFile = new ZipFile(zipPath, Charset.forName("GBK")); final ZipFile zipFile = new ZipFile(zipPath, Charset.forName("GBK"));
File file = new File(outPath); final File file = new File(outPath);
// 限制解压文件大小为637KB // 限制解压文件大小为637KB
long size = 637*1024L; final long size = 637*1024L;
// 限制解压文件大小为636KB // 限制解压文件大小为636KB
// long size = 636*1024L; // long size = 636*1024L;