--- tags: - security - vulnerability - poc - skops - numpy - sklearn - huggingface - decompression-bomb - cwe-789 license: mit --- # skops — .skops NPY Dtype Decompression Bomb (PoC) **Repo:** `MBM7/skops-dtype-decompression-bomb-poc` **Status:** Responsible disclosure — submitted to Huntr **Severity:** High / CWE-789 **Package:** `skops` (PyPI) — HuggingFace official sklearn model format --- ## Summary A crafted **8.9 KB** `.skops` sklearn model file causes `skops.io.loads()` to allocate **2 GB of memory** before failing. | File size | Claimed dtype itemsize | Peak allocation | Amplification | |-----------|------------------------|-----------------|---------------| | 8,935 bytes | 500,000,000 | **500 MB** | 1 : 55,959 | | 8,935 bytes | 2,000,000,000 | **2,000 MB** | 1 : 223,838 | --- ## Root Cause `skops/io/_numpy.py`, `NdArrayNode._construct()`: ```python content = np.load( self.children["content"], # BytesIO of .npy file from .skops ZIP allow_pickle=False ) # ← np.load allocates count * dtype.itemsize BEFORE reading data ``` The `.skops` format is a ZIP archive containing `schema.json` + `.npy` files. When loading, `NdArrayNode` reads each `.npy` file into a `BytesIO` and passes it directly to `np.load()` — **with no validation of dtype.itemsize or shape**. A crafted `.npy` file inside the ZIP claiming `dtype=|S2000000000` (2 GB itemsize) causes `np.load()` to pre-allocate 2 GB before attempting to read data. --- ## Attack Replace any `.npy` file inside a legitimate `.skops` model archive with a dtype bomb `.npy` header. `schema.json` remains unchanged — skops trusts it and loads all `.npy` files unconditionally: ```python import skops.io as sio # Load triggers NdArrayNode._construct() → np.load() → 2GB allocation trusted = sio.get_untrusted_types(data=bomb_skops_bytes) model = sio.loads(bomb_skops_bytes, trusted=trusted) ``` --- ## Distinct from numpy NPY finding | | numpy NPY | **skops** | |--|--|--| | Entry point | `np.load('file.npy')` | `skops.io.loads(skops_bytes)` | | Format | `.npy` raw | `.skops` ZIP (sklearn model) | | Ecosystem | NumPy arrays | HuggingFace sklearn model sharing | | Context | Direct array loading | sklearn model deserialization | --- ## Reproduce ```bash pip install skops scikit-learn numpy python poc_skops_dtype_bomb.py ``` Expected: ``` .skops file size : 8,935 bytes Expected allocation: 2,000,000,000 bytes Amplification : 1:223,838 Result : ValueError: EOF: reading array data... Peak memory : 2000 MB ← allocated before error ``` --- ## Impact Any service loading user-supplied `.skops` files from HuggingFace Hub or other sources can be OOM-killed by a sub-10KB payload. skops is the **official HuggingFace format** for sharing sklearn models — directly relevant to the AI/ML supply chain. --- ## Suggested Fix In `NdArrayNode._construct()`, validate before `np.load()`: ```python MAX_ARRAY_BYTES = 512 * 1024 * 1024 # 512 MB buf = self.children["content"] # Peek at NPY header without allocating hdr = np.lib.format.read_array_header_1_0(buf) buf.seek(0) if hdr[1].itemsize > MAX_ARRAY_BYTES: raise ValueError( f"NPY dtype itemsize {hdr[1].itemsize} exceeds limit — " "possible decompression bomb" ) content = np.load(buf, allow_pickle=False) ``` --- ## Environment | Package | Version | |---------|---------| | skops | 0.14.0 | | numpy | 2.4.4 | | Python | 3.12 | --- *skops was created as a **safer alternative to pickle** for sklearn models.* *Ironic: the safety-focused format has an unvalidated allocation path.*