ModelScan bypass PoC β€” legacy (non-zip) PyTorch .pt, payload behind the magic-number pickle

Authorized security research. Benign marker payload only (touch /tmp/POC_PWNED_torch). For huntr.com Model File Format disclosure.

Summary

ModelScan's PyTorch scanner (scan_pytorch β†’ scan_pickle_bytes(..., multiple_pickles=False)) reads only the first pickle stream of a .pt file. PyTorch's legacy serialization format (torch.save(obj, f, _use_new_zipfile_serialization=False)) is a sequence of separate concatenated pickle streams: pickle(MAGIC_NUMBER), pickle(PROTOCOL), pickle(sys_info), then pickle(payload), then storages.

Because multiple_pickles=False stops after the first stream (the magic number), the malicious __reduce__ payload in a later stream is never parsed. ModelScan reports the file clean β€” total_issues: 0, errors: [], scanned: 1 (it believes it fully scanned a clean file). torch.load(f, weights_only=False) still executes the payload.

This is distinct from the publicly documented PyTorch legacy attacks, which are tar-based (3 files in a tar) or rely on crashing the scanner via ZipFile/STACK_GLOBAL exceptions. This variant is a flat, non-tar stream that scans clean with zero errors.

Files

  • evil_legacy.pt β€” the bypass: flat legacy .pt, payload in a later pickle stream. ModelScan: clean.
  • evil.pt β€” control: identical payload in the modern zip format. ModelScan: flagged CRITICAL os.system.
  • build.py β€” builds both.

Reproduce

pip install modelscan torch
python build.py
modelscan -p evil_legacy.pt   # => "No issues found!", Total Issues 0   (BYPASS)
modelscan -p evil.pt          # => Total Issues 1, CRITICAL os.system   (control, caught)
python -c "import torch; torch.load('evil_legacy.pt', weights_only=False)"  # marker /tmp/POC_PWNED_torch created

Isolation proof

Same file, only the scan flag differs:

  • _list_globals(..., multiple_pickles=False) β†’ globals found: set() (what ModelScan does)
  • _list_globals(..., multiple_pickles=True) β†’ globals found: {('os','system')}

Confirms the bypass is the container/scan-flag, not the payload.

Threat model

Requires torch.load(weights_only=False). torch β‰₯2.6 defaults weights_only=True, but weights_only=False is exactly the scenario ModelScan exists to defend, and remains common (e.g. NeMo forces it False; much existing code passes it). Tested: modelscan 0.8.6, torch 2.8.0, Python 3.9.

Fix

In scan_pytorch, scan with multiple_pickles=True for legacy-format .pt files (iterate every concatenated pickle stream), not just the first.

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