MBM7's picture
Upload 2 files
7bec85f verified
|
Raw
History Blame Contribute Delete
3.68 kB
---
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.*