ModelScan legacy-PyTorch scanner bypass β PoC (huntr responsible disclosure)
Access is gated with manual review; shared with protectai-bot for triage.
The bug
ModelScan fails to detect a malicious payload in a PyTorch file saved with
the legacy (non-zip) serialization format
(_use_new_zipfile_serialization=False).
That format writes five concatenated pickle streams into one file. ModelScan scans only the first one:
| Project | Legacy-format scan |
|---|---|
picklescan (scanner.py:609-610) β the tool ModelScan ports from |
for _ in range(5): scan_result.merge(scan_pickle_bytes(..., multiple_pickles=False)) |
ModelScan (modelscan/tools/picklescanner.py:269) |
return scan_pickle_bytes(model, settings, scan_name, multiple_pickles=False) β single call, no loop |
The payload lands in a later pickle stream, so it is never inspected.
Demonstration (A/B β same payload, only the format differs)
| File | ModelScan verdict |
|---|---|
legacy_bypass.pt |
"No issues found! π" |
modern_control.pt |
CRITICAL β Total Issues: 1 |
The control proves os.system is on ModelScan's denylist. The bypass is the
format, not an unknown gadget.
torch.load("legacy_bypass.pt", weights_only=False) then executes the
payload β verified (marker file created).
Files
| File | Purpose |
|---|---|
legacy_bypass.pt |
271-byte legacy-format file β not detected |
modern_control.pt |
Identical payload, modern format β detected CRITICAL |
build_poc.py |
Regenerates both |
Safety
The payload runs touch /tmp/MODELSCAN_MISSED_ME.txt β it creates one empty
marker file and nothing else. No network access, no destructive operation, no
persistence. Substituting that string is all that separates this from a real
attack, which is precisely the point: the scanner does not see it either way.
Tested: modelscan 0.8.8, torch 2.13.0, Python 3.11.