--- tags: - security - vulnerability - poc - keras - h5py - hdf5 - decompression-bomb - cwe-789 - safe-mode-bypass license: mit --- # Keras HDF5 — Sparse Weight Dataset Decompression Bomb (PoC) **Repo:** `MBM7/keras-hdf5-sparse-weight-bomb-poc` **Status:** Responsible disclosure — submitted to Huntr **Severity:** High / CWE-789 — safe_mode=True bypass **Packages:** `keras` + `h5py` (PyPI) --- ## Summary A crafted **95 KB** `.h5` Keras model file causes `keras.models.load_model()` to allocate **2 GB of memory** and OOM-kill the process — even with `safe_mode=True`. | File size | Claimed weight size | Result | Amplification | |-----------|---------------------|-----------------|---------------| | 95 KB | 2,000,000,000 bytes | OOM kill (Killed) | 1 : 20,925 | --- ## Root Cause HDF5 chunked datasets support **sparse storage** — unwritten chunks exist only as metadata. A weight dataset can claim `shape=(500_000_000,)` float32 (= 2 GB) while storing only a single 40-byte chunk on disk. When `load_weights_from_hdf5_group()` reads the dataset: ```python # h5py/_hl/dataset.py — triggered by ds[...] in Keras weight loading arr = numpy.empty(self.shape, dtype=self.dtype) # ← 2 GB allocated HERE self.id.read(mspace, fspace, arr, ...) # fill from sparse chunks ``` **No shape validation** exists in Keras before h5py allocates memory. `safe_mode=True` only blocks Lambda/pickle deserialization — **not** weight sizes. --- ## Attack — call chain ``` keras.models.load_model(path, safe_mode=True) → load_model_from_hdf5() [legacy_h5_format.py] → load_weights_from_hdf5_group() [legacy_h5_format.py ~213] → g[weight_name][...] ← h5py allocates ds.shape bytes NO bounds check before this call ``` --- ## Reproduce ```bash pip install keras h5py tensorflow-cpu python poc_keras_hdf5_sparse_bomb.py ``` Expected output: ``` Malicious HDF5 : 95,576 bytes Claimed weight size: 2,000,000,000 bytes (2.0 GB) Amplification : 1 : 20,925 safe_mode=True : True (attack bypasses safe_mode entirely) Loading with keras.models.load_model(safe_mode=True) ... Killed ``` --- ## Impact Any service loading user-supplied `.h5` Keras models is vulnerable: - `keras.models.load_model(path, safe_mode=True)` — **bypassed** - HuggingFace model hub downloads, Keras model zoos, ML pipelines - `safe_mode=True` provides no protection against this attack vector --- ## Suggested Fix In `load_weights_from_hdf5_group()`, validate before reading: ```python MAX_WEIGHT_BYTES = 512 * 1024 * 1024 # configurable for weight_name in ...: ds = group[weight_name] if ds.size * ds.dtype.itemsize > MAX_WEIGHT_BYTES: raise ValueError( f"Weight '{weight_name}' claims {ds.size * ds.dtype.itemsize} bytes " f"— exceeds safety limit. Possible decompression bomb." ) weights.append(ds[...]) ``` --- ## Environment | Package | Version | |---------------|----------| | keras | 3.15.0 | | h5py | 3.14.0 | | Python | 3.12 | --- *Discovered via UBDAF automated scanner + empirical verification. Distinct from Keras Lambda exploit (CWE-502) — this is a weight storage attack at the HDF5 layer, independent of model config deserialization.*