Apache Commons Compress防Zip Bomb未生效问题排查
现有基于java.util.zip库实现的Zip文件解压Java代码,在SonarQube扫描时被检出安全热点漏洞,提示存在Zip Bomb(压缩包炸弹)安全风险,告警位置在代码行ZipEntry entry = zipIn.getNextEntry();,告警信息为:
“Make sure that expanding this archive file is safe here(请确认在此处解压归档文件的操作具备安全性)”
为修复该问题,开发人员尝试替换为Apache Commons Compress 1.21版本实现解压逻辑——该组件自1.17版本起已内置Zip Bomb防护能力。为验证防护效果,开发人员使用公开的Zip Bomb测试样本开展验证测试,但测试过程中恶意压缩包被正常解压,全程未抛出任何错误或异常,需要排查该Commons Compress解压代码存在的问题。
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-compress</artifactId> <version>1.21</version> </dependency>
private void unzipNormal(String zipFilePath, String destDirectory) { try { File destDir = new File(destDirectory); if(!destDir.exists()) { destDir.mkdir(); } try(ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath))) { ZipEntry entry = zipIn.getNextEntry(); while(entry != null) { String filePath = destDirectory + File.separator + entry.getName(); if(!entry.isDirectory()) { extractFile(zipIn, filePath); } else { File dir = new File(filePath); dir.mkdir(); } zipIn.closeEntry(); entry = zipIn.getNextEntry(); } zipIn.close(); } } catch (Exception ex) { ex.printStackTrace(); } } private static void extractFile(ZipInputStream zipIn, String filePath) throws IOException { try(BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))) { byte[] bytesIn = new byte[4096]; int read = 0; while((read = zipIn.read(bytesIn)) != -1) { bos.write(bytesIn, 0, read); } bos.close(); } catch (Exception ex) { ex.printStackTrace(); throw ex; } }
private void unzip(String srcZipFile, String destFolder) throws IOException { Path filePath = Paths.get(srcZipFile); try(InputStream inputStream = Files.newInputStream(filePath); ZipArchiveInputStream i = new ZipArchiveInputStream(inputStream) ) { System.out.println("Begin.."); ArchiveEntry entry = null; while((entry = i.getNextEntry()) != null) { if(!i.canReadEntryData(entry)) { System.out.println("Continue.."); continue; } Path path = Paths.get(destFolder, entry.getName()); File f = path.toFile(); if(entry.isDirectory()) { if (!f.isDirectory() && !f.mkdirs()) { throw new IOException("failed to create directory " + f); } } else { File parent = f.getParentFile(); if(!parent.isDirectory() && !parent.mkdirs()) { throw new IOException("failed to create directory " + parent); } try (OutputStream o = Files.newOutputStream(f.toPath())) { IOUtils.copy(i, o); } } } } catch (Exception ex) { ex.printStackTrace(); } }
这段代码没有拦截到Zip Bomb的核心原因非常直接:Apache Commons Compress的Zip Bomb防护能力默认是关闭的。
代码中使用new ZipArchiveInputStream(inputStream)这个最简构造方法实例化解压流时,没有传入任何安全阈值配置,组件内置的压缩比异常检测、单文件解压大小上限、总解压大小上限这些防护逻辑全程不会触发,恶意压缩包自然会被正常解压。
Commons Compress的防护规则需要开发者结合自身业务场景手动配置才能生效,三个核心配置阈值如下:
- 最大压缩比:解压后文件大小/压缩包内对应条目原始大小的比值,超过该值则判定为高压缩比恶意文件,直接抛出异常终止解压
- 单条目解压上限:单个压缩条目解压后的大小超过设定值时直接拦截
- 总解压大小上限:整个压缩包所有内容解压后的累计大小超过阈值时终止解压
构造ZipArchiveInputStream时传入自定义的阈值配置即可开启防护,示例代码如下:
// 根据业务场景配置安全阈值:此处示例为最大压缩比100,单文件最大1GB,总解压大小最大10GB ThresholdIterator thresholds = Thresholds.builder() .withMaxCompressionRatio(100) .withMaxEntrySize(1024L * 1024 * 1024) .withMaxTotalSize(10L * 1024 * 1024 * 1024) .get(); try(InputStream inputStream = Files.newInputStream(filePath); ZipArchiveInputStream zipIn = new ZipArchiveInputStream(inputStream, "UTF-8", true, false, thresholds) ) { // 原有解压逻辑保持不变 }
配置完成后,当解压的压缩包触发任意阈值规则时,代码会抛出ZipException终止解压流程,实现Zip Bomb防护。
内容的提问来源于stack exchange,提问作者Vasanth Subramanian

