|
Download README.md from johnsmithle/modelscan-numpy2-scanner-failure: direct link, hf CLI and curl.
- Browser
- Download file 4.01 kB
-
https://huggingface.co/johnsmithle/modelscan-numpy2-scanner-failure/resolve/main/README.md
- Command line
-
hf download hf://johnsmithle/modelscan-numpy2-scanner-failure/README.md
-
curl -L -H "Authorization: Bearer $HF_TOKEN" -o README.md https://huggingface.co/johnsmithle/modelscan-numpy2-scanner-failure/resolve/main/README.md
4.01 kB
| license: apache-2.0 | |
| tags: | |
| - security | |
| - proof-of-concept | |
| - modelscan | |
| # PoC β ModelScan's NumPy scanner is dead on numpy β₯ 2.0 | |
| **Security research artefact for a huntr Model File Vulnerability report. Not a | |
| usable model. Do not load this file outside a disposable sandbox.** | |
| ## What is in this repo | |
| `embeddings.npy` β 431 bytes, sha256 | |
| `c021a6fb90c674253a10cebfca50e16cb723fabf7e6b3ae39370fc47dc2dfa7f` | |
| An ordinary object-dtype NumPy array. `.npy` stores object arrays by embedding a | |
| pickle stream after the header, so `np.load(..., allow_pickle=True)` unpickles β | |
| and executes β it. **The payload is inert**: it creates `/tmp/huntr_exec/NPY` and | |
| nothing else. It is a stand-in for arbitrary code. | |
| ## The issue | |
| `modelscan/tools/picklescanner.py:228-237`: | |
| ```python | |
| elif magic == np.lib.format.MAGIC_PREFIX: | |
| # .npy file | |
| 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 removed | |
| if dtype.hasobject: | |
| return scan_pickle_bytes(model, settings, scan_name, True, stream.tell()) | |
| else: | |
| return ScanResults([], [], []) | |
| ``` | |
| Both `_check_version` and `_read_array_header` are **private** numpy helpers and | |
| were removed in numpy 2.0. On numpy β₯ 2 the first call raises `AttributeError`. | |
| ModelScan catches it, records it under `--- Errors ---`, and still prints: | |
| ``` | |
| --- Summary --- | |
| No issues found! π | |
| ``` | |
| The exception is raised **before** the `dtype.hasobject` branch, so every `.npy` | |
| file is affected β benign and malicious alike. The NumPy scanner is effectively | |
| disabled. | |
| `pyproject.toml` declares `numpy = ">=1.24.3"` with no upper bound, so a fresh | |
| `pip install modelscan` resolves numpy 2.x and ships this state by default. | |
| ## Reproduction | |
| Same modelscan 0.8.8, same file, only numpy differs: | |
| ```bash | |
| # numpy 1.x β scanner works | |
| pip install modelscan "numpy<2" | |
| modelscan -p embeddings.npy | |
| # Total Issues: 1 | |
| # - Severity: CRITICAL | |
| # - Description: Use of unsafe operator 'exec' from module 'builtins' | |
| # numpy 2.x β scanner is dead | |
| pip install modelscan "numpy>=2" | |
| modelscan -p embeddings.npy | |
| # No issues found! π | |
| # --- Errors --- | |
| # module 'numpy.lib.format' has no attribute '_check_version' | |
| ``` | |
| Confirming the file really executes (run in a sandbox): | |
| ```python | |
| import numpy as np | |
| np.load("embeddings.npy", allow_pickle=True) | |
| # -> /tmp/huntr_exec/NPY now exists | |
| ``` | |
| A benign `np.save("x.npy", np.arange(10))` triggers the same `AttributeError`, | |
| which confirms the failure is universal rather than payload-specific. | |
| ## Measured results | |
| | modelscan | numpy | verdict | | |
| |---|---|---| | |
| | 0.8.8 | 1.26.4 | **Total Issues: 1 β CRITICAL** | | |
| | 0.8.8 | 2.4.6 | **No issues found** (scanner raised) | | |
| ## Impact | |
| This is not an evasion an attacker has to craft β it is a silent regression that | |
| disables an entire scanner class by default. Anyone who installed ModelScan after | |
| numpy 2.0 shipped (June 2024) has had zero `.npy` coverage while the tool | |
| reported success. The `--- Errors ---` section is easy to miss, and CI normally | |
| keys on the exit status or issue count rather than on stderr noise. | |
| ## Scope note | |
| Executing a malicious `.npy` still requires the loading code to pass | |
| `np.load(..., allow_pickle=True)`; numpy has defaulted that to `False` since | |
| 1.16.5. So this is not unauthenticated RCE on its own. It is a complete failure | |
| of the control that exists to catch exactly the `allow_pickle=True` case, which | |
| remains common for object arrays, legacy datasets and research pipelines. | |
| ## Suggested fix | |
| Stop calling numpy private APIs β `numpy.lib.format.read_array_header_1_0` / | |
| `read_array_header_2_0` are public, or parse the dtype from the header dict. | |
| Separately: cap the dependency (`numpy>=1.24.3,<3`), and make a scanner that | |
| raises during a scan produce a failing or inconclusive result instead of being | |
| folded into "No issues found". | |