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():

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:

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

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():

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.

Downloads last month
-
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support