PoC for Huntr Report β€” NumPy Compatibility Regression Can Lead to Fail-Open Integrations by Reporting Zero Issues for Unscanned Malicious NPY/NPZ Files

Target: ModelScan (ProtectAI) β€” github.com/protectai/modelscan, the scanning tool huntr's own Model File Vulnerability program names as the reference defense ("Unique methods to bypass our existing model scanning tools"). Reported here through the NPY / NPZ huntr targets, since the concrete demonstration is a malicious .npy/.npz file that the tool fails to flag.

CWE: CWE-755 (Improper Handling of Exceptional Conditions), secondary CWE-693 (Protection Mechanism Failure)

Affected: modelscan==0.8.8 (current, latest on PyPI) with numpy>=2.3.0 (current, latest is 2.4.6). modelscan's own pyproject.toml declares numpy = ">=1.24.3" with no upper bound, so this combination is what a completely standard pip install modelscan produces today.

Why this is security-relevant

This is not simply a NumPy compatibility bug. ModelScan's entire purpose is to tell a user or a pipeline whether a model file is safe to load. The failure happens inside a security control, and the control's own output actively reports a clean result:

  • summary.total_issues is 0.
  • scanned.total_scanned is 0 (nothing was actually scanned).
  • A separate errors array records the real cause.

A workflow, script, or reviewer that checks whether issues were reported (total_issues == 0 in JSON, or reads the prominent "No issues found! πŸŽ‰" summary line in the console output) without separately checking errors/scanned or the process exit code can reasonably conclude the file is safe, when in fact it was never scanned at all. See "Impact and honest scope" below for exactly what is and is not proven about how likely that is.

1. The regression, precisely bounded

ModelScan's NumPy/pickle scanner (modelscan/tools/picklescanner.py, scan_numpy()) calls a private NumPy internal function, numpy.lib.format._check_version(), to validate the .npy header before deciding whether the array is object-dtype (and therefore needs pickle-opcode scanning). This function was removed from NumPy's public-ish internals at a specific version boundary, found here by direct binary search across released versions:

NumPy version numpy.lib.format._check_version
1.24.3 – 2.2.0 Present β€” scanner works
2.3.0 and later (including current 2.4.6) Absent β€” scanner raises AttributeError and never completes

modelscan's declared dependency (numpy >= 1.24.3, no ceiling) does not prevent installing the broken combination.

2. A/B proof: this is a regression, not "never worked"

The identical PoC file (evil_bypass.npy, produced by generate_poc.py) was scanned against both environments, changing only the installed NumPy version:

With NumPy 2.2.0 (compatible):

Scanning .../evil_bypass.npy using modelscan.scanners.NumpyUnsafeOpScan model scan
--- Summary ---
Total Issues: 1
    - CRITICAL: 1
--- CRITICAL ---
Unsafe operator found:
  - Description: Use of unsafe operator 'system' from module 'nt'
EXIT_CODE=1

With NumPy 2.4.6 (current):

Error encountered from scanner modelscan.scanners.NumpyUnsafeOpScan with path .../evil_bypass.npy:
module 'numpy.lib.format' has no attribute '_check_version'
--- Summary ---
 No issues found! πŸŽ‰
--- Errors ---
Error 1: module 'numpy.lib.format' has no attribute '_check_version'
EXIT_CODE=3

Full captured output for both: raw_output_compatible_numpy.txt, raw_output_current_numpy.txt.

This confirms ModelScan does correctly detect this exact payload when its dependency is compatible -- the finding is specifically about the current, unpinned-numpy state of the tool, not a claim that ModelScan never had NPY/NPZ detection capability.

3. JSON / structured output

{
  "summary": {
    "total_issues_by_severity": {"LOW": 0, "MEDIUM": 0, "HIGH": 0, "CRITICAL": 0},
    "total_issues": 0,
    "scanned": {"total_scanned": 0}
  },
  "issues": [],
  "errors": [
    {"category": "MODEL_SCAN", "description": "module 'numpy.lib.format' has no attribute '_check_version'", "source": "evil_bypass.npy"}
  ]
}

Full output: raw_output_json.txt, scan_result.json. total_issues: 0 and scanned.total_scanned: 0 sit in summary, structurally separate from errors. This is verified, structured evidence that a consumer of this JSON has to specifically check the errors array (or the process exit code) to learn the scan did not complete -- checking total_issues alone is not sufficient to conclude the file was scanned and found clean.

4. Mixed-directory scan (more realistic than a single isolated file)

A directory containing one benign file (clean_weights.npy) and one malicious file (model_weights.npy, same payload as evil_bypass.npy) was scanned together:

Error encountered ... clean_weights.npy: module 'numpy.lib.format' has no attribute '_check_version'
Error encountered ... model_weights.npy: module 'numpy.lib.format' has no attribute '_check_version'
--- Summary ---
 No issues found! πŸŽ‰
--- Errors ---
Error 1: ...
Error 2: ...
EXIT_CODE=3

Full output: raw_output_mixed_directory.txt. Behavior is identical to the single-file case: both files fail to scan for the same reason, and the summary banner is unchanged.

Impact and honest scope

  • Exit code: non-zero (2 or 3 depending on scenario) in every case tested here. A CI/CD gate that checks the process exit code for zero is not bypassed at that level -- this was directly tested, not assumed.
  • JSON total_issues field: 0, structurally separate from the errors array, in every case tested here. This is a real, reproducible machine-readable field that would read as "clean" to any consumer that checks it without also checking errors or scanned.total_scanned.
  • Console summary: the "No issues found! πŸŽ‰" line is the first and most prominent output in every case tested here, with the actual error only visible in a separate section below it.
  • This report does not claim that any specific real-world integration only checks total_issues, or that CI gates are broadly defeated -- that was not tested and is not asserted. The claim is scoped to what was directly reproduced: NPY/NPZ scanning silently fails to complete on the current supported NumPy range, and the tool's own summary output (both console and JSON) can lead to fail-open behavior in integrations that rely on summary.total_issues without checking reported scan errors.

Environment

modelscan==0.8.8
numpy==2.4.6 (broken) / numpy==2.2.0 (compatible, for the A/B test)
Python 3.11.15
Windows 11 Home 10.0.26200 (Build 26200)

Files in this repository

File Purpose
generate_poc.py Builds evil_bypass.npy, evil_bypass.npz, and clean_weights.npy.
evil_bypass.npy, evil_bypass.npz The PoC malicious files (harmless payload -- only creates a local marker file).
clean_weights.npy Benign file used in the mixed-directory test.
mixed_scan_dir/ The mixed-directory scenario (one clean, one malicious file).
raw_output_current_numpy.txt Console output, single file, numpy 2.4.6 (broken).
raw_output_compatible_numpy.txt Console output, single file, numpy 2.2.0 (compatible) -- the A/B proof.
raw_output_json.txt, scan_result.json JSON structured output, numpy 2.4.6 (broken).
raw_output_mixed_directory.txt Console output, mixed directory, numpy 2.4.6 (broken).

Reproduction

pip install "modelscan==0.8.8" numpy psutil
python generate_poc.py

# Broken (current numpy):
modelscan -p evil_bypass.npy
modelscan scan -p evil_bypass.npy -r json -o scan_result.json
modelscan -p mixed_scan_dir

# A/B: install a compatible numpy and repeat the first command
pip install "numpy==2.2.0"
modelscan -p evil_bypass.npy

Suggested fixes

  • Stop relying on the private/internal numpy.lib.format._check_version; use NumPy's public API for .npy header validation, or vendor the minimal parsing logic needed.
  • Cap the numpy dependency range in pyproject.toml to versions actually tested against, until the above is fixed.
  • When a scanner raises an exception, do not present a clean "No issues found" summary; surface unscanned/errored files as a distinct, non-clean top-level state (both in the console banner and in the JSON summary), separate from "scanned and found zero issues."
  • Add a CI regression test that scans a known-malicious .npy/.npz fixture against the full supported NumPy version range and asserts total_issues > 0, not just that the process doesn't crash.
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support