YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
ModelScan 0.8.x arbitrary file write via io.open alias bypass and NEWOBJ_EX kwargs injection
Proof of concept for the Protect AI / huntr.com Model Format Vulnerability program.
Vulnerability
Two independent weaknesses combined into an arbitrary file write chain.
Weakness 1: io.open alias bypass for builtins.open
ModelScan blocks builtins.open at CRITICAL severity. However, io.open is the identical function object (io.open is open evaluates to True in Python), and the io module is entirely absent from the unsafe_globals blocklist.
Referencing builtins.open as io.open produces the global ('io', 'open') in the pickle stream. This is not matched by the blocklist rule "builtins": ["open"]. ModelScan reports clean despite calling the exact same blocked function.
Weakness 2: NEWOBJ_EX opcode enables kwargs in pickle without operator.methodcaller
Pickle protocol 4 includes the NEWOBJ_EX opcode (0x92), which calls cls.new(cls, *args, **kwargs) with full keyword-argument support. This opcode is not covered in any known ModelScan bypass research.
Using NEWOBJ_EX to construct functools.partial with a file= keyword argument allows calling builtins.print(content, file=fileobj) at deserialization time, writing arbitrary content to the opened file.
builtins.print is not in the blocklist. Only eight specific builtins are listed: eval, compile, getattr, apply, exec, open, breakpoint, import.
Combined chain
io.open(path, 'a') -> file_obj
NEWOBJ_EX(
functools.partial,
args=(builtins.print, content),
kwargs={file: file_obj, end: ''}
) -> partial_obj
partial_obj() -> print(content, file=file_obj)
-> writes content to path
Globals extracted by ModelScan:
('io', 'open') not in unsafe_globals
('functools', 'partial') not in unsafe_globals
('builtins', 'print') not in unsafe_globals
ModelScan verdict: No issues found.
Affected
- Scanner: ModelScan 0.8.8 (latest as of July 2026)
- Extensions: .pkl .pickle .joblib .dill .dat .data
- Trigger: pickle.loads() and joblib.load()
Reproduction
Install requirements:
pip install modelscan joblib
Generate the PoC files:
python3 poc_io_open_filewrite.py
This creates poc_filewrite_bypass.pkl and poc_filewrite_bypass.joblib and confirms that the target file is written at deserialization time.
Scan with ModelScan:
modelscan -p poc_filewrite_bypass.pkl
Expected output: No issues found
Attack scenarios
Writing to ~/.ssh/authorized_keys with an attacker public key gives persistent SSH access. Writing to /etc/cron.d/ (as root) gives scheduled code execution. Writing to ~/.bashrc or ~/.profile gives code execution on the next shell session. Replace WRITE_TARGET and WRITE_CONTENT in the PoC script with the desired path and content.
Payload structure
io.open(path, 'a') -> file_obj [memo 6]
builtins.print -> print_func [memo 9]
functools.partial -> partial_cls [memo 12]
NEWOBJ_EX(partial_cls,
(print_func, content),
{file: file_obj, end: ''}) -> partial_obj
EMPTY_TUPLE
REDUCE(partial_obj, ()) -> print(content, file=file_obj) -> file write
STOP
Root Cause
The blocklist approach has two gaps demonstrated here.
First, it matches function references by module name and attribute name, but does not account for the same function being accessible under multiple module paths. builtins.open and io.open refer to the same underlying C function but only the former is blocked.
Second, the NEWOBJ_EX opcode (protocol 4) is not analyzed for kwargs-based dangerous construction. It enables creating functools.partial with arbitrary keyword arguments including file handles, bypassing the requirement for operator.methodcaller or operator.attrgetter.
Fix
Add to unsafe_globals:
"io": ["open"],
"_io": ["open"],
"functools": "*",
Or alternatively, build a whitelist approach rather than a denylist.
Files
- poc_io_open_filewrite.py: generates and verifies both PoC files
- poc_filewrite_bypass.pkl: malicious pickle, passes ModelScan, writes to disk
- poc_filewrite_bypass.joblib: same payload with .joblib extension
Ethics
Payload writes a benign string to /tmp/ml_filewrite_indicator. Submitted exclusively through the Protect AI / huntr.com bug bounty program.