ZipInputStream解压恶意压缩包引发内存暴涨的本质是未设限的流式读取与输出组合陷阱。关键在于检查read()直通操作、ByteArrayOutputStream缓存全量数据、ZipEntry.getSize()被绕过及STORED+ZIP64隐式透传等四类漏洞,并通过GC日志、内存快照和本地内存分析定位。
zipinputstream 解压恶意压缩包时出现“瞬间物理内存黑洞”,本质不是 zipinputstream 主动分配巨量内存,而是它被诱导进入无节制流式读取 + 未设限解压输出的组合陷阱。定位关键不在“它占了多少内存”,而在于它让谁、在什么条件下、把多少解压后数据写进了哪。
以下是从现象反推根源的实操定位路径:
看解压循环里有没有无保护的 read() 直通操作
这是最常见黑洞源头。典型错误模式是:
- 用固定大缓冲区(如
byte[8192])反复zis.read(buffer) - 不检查
entry.getSize(),也不限制单文件解压上限 - 把读到的数据直接
write()到FileOutputStream或ByteArrayOutputStream
一旦遇到 zip bomb(比如 42.zip 这类多层嵌套、高倍率膨胀的包),zis.read() 会持续返回有效字节,而你的代码照单全收——操作系统内核会为后续写入分配页内存,表现为 RSS 快速飙升,甚至触发 OOM Killer。
检查是否误用 ByteArrayOutputStream 或 List<byte[]> 缓存全部内容
有些实现为了“先校验再写盘”,会把每个 entry 全部读进 ByteArrayOutputStream.toByteArray()。一个标称 50KB 的 zip bomb 条目,解压后可能膨胀至 GB 级,而 toByteArray() 必须一次性申请连续堆内存,极易触发 Full GC 或直接 OutOfMemoryError。此时 jstat -gc 会看到 Eden 区瞬时打满、老代快速晋升。
观察 ZipEntry.getSize() 是否被绕过或失效
zip bomb 常利用 ZIP 规范中 size 字段可伪造/省略的特性:
- 某些炸弹条目
getSize() == -1(未知大小) - 或
getSize()返回极小值(如 100),但实际解压流源源不断
如果你的代码只依赖getSize()做长度判断,就会彻底失效。应配合运行时累计计数器,并设置硬上限(如单文件 ≤ 100MB)。
验证是否触发了 STORED 条目 + ZIP64 的隐式陷阱
Android 或旧版 JDK 中,含 STORED 条目且带 ZIP64 扩展头的 zip 包,ZipInputStream 可能跳过校验直接透传原始字节。若该条目本身是另一层压缩包(如嵌套 zip),而你又自动递归解压,就等于开了双重黑洞入口。用 unzip -l -v evil.zip 查看条目 method 和 size 字段是否异常一致,是快速初筛手段。
定位时建议三步走:
- 启动 JVM 加
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps,观察 GC 日志是否密集爆发 - 解压前加
Runtime.getRuntime().freeMemory()/totalMemory()快照,解压中每 10MB 输出一次当前已写入字节数和内存差值 - 用
jcmd <pid> VM.native_memory summary scale=MB查看本地内存分配热点(尤其Internal和Other区)
本质上,ZipInputStream 本身不制造黑洞,它只是把炸弹的引信交到了没设防的代码手里。


















