ModelScan Systemic Denylist Gap β 20 Bypass Vectors
Summary
ProtectAI ModelScan v0.8.8 has a systemic gap in its pickle denylist. 20 Python stdlib modules can be used to bypass detection, including 3 CRITICAL (full arbitrary code execution), 5 HIGH (native code / module loading), and 5 MEDIUM (network exposure / code compilation) severity bypasses.
Root Cause
ModelScan uses a denylist approach to detect malicious pickle operations. The denylist covers well-known dangerous modules (os, subprocess, builtins, shutil, sys, etc.) but misses many stdlib modules that provide equivalent code execution capabilities through indirect paths.
CRITICAL Bypasses (Full ACE)
| Module | Function | Mechanism | Size |
|---|---|---|---|
cProfile |
run() |
Calls exec(statement) internally |
~92 bytes |
profile |
run() |
Calls exec(statement) internally |
~98 bytes |
timeit |
timeit() |
Calls exec(statement) internally |
~95 bytes |
These three modules ALL internally call Python's exec() builtin on a user-supplied string,
achieving full arbitrary code execution while ModelScan reports zero issues.
HIGH Bypasses (Chain to ACE)
| Module | Function | Mechanism |
|---|---|---|
ctypes.CDLL |
Loads arbitrary .so files β native code execution |
|
marshal.loads |
Deserializes Python code objects β chain to execution | |
importlib.import_module |
Imports arbitrary modules β chain to os/subprocess | |
zipimport.zipimporter |
Imports from ZIP archives β embedded code execution | |
types.FunctionType |
Creates callable function from code object |
MEDIUM Bypasses (Network / Compilation)
| Module | Function | Mechanism |
|---|---|---|
http.server.test |
Starts HTTP server (directory listing + file serving) | |
xmlrpc.server.SimpleXMLRPCServer |
Starts XML-RPC server | |
socketserver.TCPServer |
Starts TCP server | |
doctest.run_docstring_examples |
Executes code from docstrings | |
ensurepip._main |
Bootstraps pip β can install arbitrary packages |
Reproduction
pip install modelscan==0.8.8
# Scan any bypass file β reports NO ISSUES
modelscan -p bypass_cProfile_run.pkl
# Load the same file β executes RCE payload
python3 -c "import pickle; pickle.loads(open('bypass_cProfile_run.pkl','rb').read())"
Tested Against
- ModelScan v0.8.8 (latest as of April 2026)
- Python 3.12.3
- Ubuntu 24.04
Impact
Any ML model distributed as .pkl, .bin, or .pt can embed these bypass payloads.
Users relying on ModelScan for safety scanning will see "No issues found" while the
model achieves full arbitrary code execution on load.
Fix
The denylist approach is fundamentally insufficient. Recommended:
- Add ALL identified modules to denylist immediately (patch)
- Long-term: switch to allowlist approach (only permit known-safe pickle operations)