YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
MLeap .mleap decompression bomb — PoC bundle
Gated security PoC for a huntr Model File Vulnerability submission. Contains a crafted .mleap bundle
(evil.zip) that triggers uncontrolled memory allocation when loaded by the standard MLeap library. Do not
load evil.zip without a memory limit.
- Target format: MLeap (.mleap) – Spark
- Parser:
ml.combust.bundle:bundle-ml(viaml.combust.mleap:mleap-runtime), tested0.23.0(sink present 0.21.x–0.25.x) - Repository:
github.com/combust/mleap - CWE: CWE-409 (Decompression Bomb) / CWE-400 / CWE-789 (allocation from declared size)
- Class: denial-of-service (memory exhaustion) on bundle load
The bug
The public load API BundleFile(uri).loadMleapBundle() → BundleFile.load → BundleSerializer.read calls
readInfo() first, unconditionally, before examining any other bundle content. readInfo()
(bundle-ml → ml/combust/bundle/BundleFile.scala:98) does
new String(Files.readAllBytes(bundleJson), "UTF-8").parseJson where bundleJson is a jdk.nio.zipfs Path into
the (attacker-supplied) ZIP. Files.readAllBytes pre-sizes a byte[] from the ZIP central directory's
declared uncompressed size (attacker-controlled) and inflates the DEFLATE entry into it — with no size cap,
no streaming limit, and no check against the compressed size. A tiny bundle whose bundle.json inflates huge
commits gigabytes of real JVM heap before anything else is validated.
Files
evil.zip(1,565,668 bytes) — the PoC bundle:bundle.jsonis a real zlib-DEFLATE stream that inflates to 1.5 GiB of genuinely valid JSON (not a declared-size spoof — the Inflater produces every byte).benign.zip(205 bytes) — valid-schema control (loads with flat ~114 MiB RSS).make_evil_zip.py— rebuildsevil.zipfrom scratch (streams a real DEFLATE compression).Driver.scala— calls the real public APIURI("jar:file:/abs/evil.zip").loadMleapBundle().build.sh— coursier resolve + scalac compile (self-contained).repro.sh— runs benign+evil undertime -v.cp.txt/scalac_cp.txt— coursier-resolved classpaths.evidence/— rawtime -v+ the OOM stack trace.
Reproduce
bash build.sh # coursier fetch mleap-runtime_2.12:0.23.0 + bundle-ml_2.12:0.23.0, compile Driver.scala
bash repro.sh # runs benign.zip then evil.zip under `command time -v`; writes evidence/rss.txt
Measured result (WSL2, JDK, MLeap 0.23.0)
benign.zip(205 B) → peak RSS ~114 MiB,readInfo()Success.evil.zip(1.49 MiB) @-Xmx8g→ peak RSS ~6.4 GiB,readInfo()returns Success (full sink: alloc + inflate 1.5 GiB + JSON parse + Bundle validate all complete on attacker data) — the decisive proof of genuine commit.evil.zip@ default heap /-Xmx4g→OutOfMemoryErrorfired insideFiles.readAllBytesafter committing ~2.0–2.1 GiB in ~4 s (real spike-then-ceiling, confirmed byevidence/oom_stacktrace_xmx4g.stderr).- Amplification: ~4300× on-disk file size; ~1029× on-disk→declared. 1.5 GiB is a demonstration size; it scales to OOM at attacker-chosen sizes.
Live OOM stack (default heap): Files.readAllBytes:3206 → BundleFile.readInfo(BundleFile.scala:98) → BundleSerializer.read:49 → BundleFile.load:123 → MleapSupport…loadMleapBundle:32.
Honest scope: DoS (memory exhaustion), not RCE. Tested on 0.23.0 (the version coursier resolved); the sink is structurally unchanged across 0.21.x–0.25.x.
Fix
Before Files.readAllBytes in readInfo() (and the analogous ModelSerializer reads), enforce a maximum
entry size / compression-ratio cap and stream-read with a bounded limit; reject bundles whose declared
uncompressed size exceeds a sane threshold.