Skip to content

+ Intertrace Threat ResearchTLP:CLEAR7 sources

Threat research9 min read

Malicious model files: the questions to answer before anyone calls torch.load

Model checkpoints saved with Python pickle can run code the moment they load. The questions security and ML teams ask about malicious models, answered from the published samples, hub scanner gaps, the PyTorch 2.6 change, and an intake pipeline that does not trust the file.

How attacks on AI agents work in practice, from public incidents and our own test corpora, and the controls that stop them.

August 27, 2026 · 9 min read

ML Supply ChainModel SecurityPickleSafetensors
Contents
  1. 01Is a model file really executable?
  2. 02Has anyone actually found malicious models on public hubs?
  3. 03Doesn't the hub scan for this?
  4. 04Didn't PyTorch 2.6 fix it?
  5. 05What would a compromise look like from the inside?
  6. 06What should model intake look like?
  7. 07Where does Intertrace fit?
  8. 08What do we check first?

In February 2024, JFrog described a PyTorch model in a Hugging Face repository called baller423/goober2 that opened a reverse shell the moment it was loaded with torch.load(), launching /bin/sh or powershell.exe depending on the platform [3]. Nobody had to run the model. Loading it was enough.

Security teams spent a decade learning that a package install runs code. The same lesson applies to model weights and is far less absorbed: a data scientist who would never pipe a script from the internet into a shell will call torch.load on a checkpoint from an unfamiliar account, because it feels like opening a file of numbers. For pickle-based formats it is closer to running a program. Our position is that a downloaded model should take the same road to production as third-party code, and that a clean scan is never permission to load an untrusted pickle somewhere that matters. These are the questions we hear when teams start, answered from the public record.

Is a model file really executable?

If it is a pickle, yes. Python's documentation opens its pickle page by warning that the module is not secure, that you should only unpickle data you trust, and that malicious pickle data can execute arbitrary code during unpickling [1]. The cause is the object-reconstruction protocol. A class can define __reduce__, which returns a callable and a tuple of arguments, and on load the unpickler calls that callable with those arguments to rebuild the object [1].

An attacker needs no real class. They write a pickle whose reduce step names a function such as os.system or builtins.exec, with a shell command or Python source as the argument. Hugging Face's explanation shows it at the opcode level: GLOBAL and STACK_GLOBAL tell the unpickler to import something, and REDUCE tells it to call that thing with the arguments on the stack [2]. By default the unpickler will import any class or function the data references [1].

How a model download becomes code executionFig. 1
Model hub download.bin / .pt / .pkltorch.load / pickle.loaddeserializes objects__reduce__ runsattacker-chosen callableCode on your hostshell, beacon, theftsafetensors: tensors only, no codea header and raw tensor bytes; loading it cannot call a functionPREFER FOR EVERY DOWNLOADloading a pickle is running a program · the file extension does not tell you which one

A .bin or .pt file from a model hub goes to torch.load or pickle.load, the __reduce__ step calls an attacker-chosen function, and code runs on your host as a shell or a beacon. Safetensors stores tensors only, so it has no step that can run code.

Timing is what makes it dangerous. The payload runs inside the load call, before your code inspects the model, before evaluation, before anything you would call using it. A try block around the load does not help, because the code has already run by the time an exception could be raised.

Has anyone actually found malicious models on public hubs?

Yes. Two research teams have published samples, with caveats about intent. JFrog reported around 100 malicious models on Hugging Face after excluding false positives, with PyTorch models the most common and TensorFlow Keras models close behind [3]. The goober2 shell connected to an address belonging to a South Korean research network, which JFrog treated as possible evidence of researcher activity rather than proof of a criminal campaign [3].

In February 2025, ReversingLabs described a technique it named nullifAI in two models, glockr1/ballr7 and a repository named with a long string of zeros [4]. Both were PyTorch files compressed with 7z instead of PyTorch's usual ZIP, so torch.load could not open them as-is. Both were also deliberately broken pickles: the malicious code sat at the start of the stream, and because deserialization executes opcodes as it reads them, the payload ran before the parser hit the error [4]. ReversingLabs judged them more likely a proof of concept for testing the method than an active attack. Hugging Face removed them within 24 hours of the January 20, 2025 report and updated Picklescan to handle broken pickles [4].

What was observed, then: working payloads on a public hub, and a scanner evasion. What was not established in either report is a criminal campaign behind them.

Doesn't the hub scan for this?

It scans, and it does not block. JFrog noted that Hugging Face's pickle scanning marks unsafe models but users can still download and run them [3], and Hugging Face's own documentation calls its pickle import scan not 100% foolproof and makes safety the user's responsibility [2]. A warning label helps a careful person and does nothing for an automated job that downloads by repository name.

Scanners also face antivirus's structural problem: they look for known-bad patterns in a format designed to run code. ReversingLabs named three weaknesses in Picklescan: a blocklist of dangerous functions it called not scalable or adaptable, validation before scanning so a broken file produced an error instead of a finding, and an unusual container that hid the pickle entirely [4]. Each generalizes.

  • Blocklists lag. Any importable function that can run a command or evaluate a string is a candidate, and Python warns that even a restricted unpickler needs care because functions like builtins.eval can be abused [1].
  • Parsers and loaders disagree. If a scanner refuses a malformed file that the loader executes up to the point of failure, the attacker lives in the gap [4].
  • Containers hide contents. A checkpoint is an archive around a pickle, and a scanner that does not unpack it the way the loader does sees nothing [4].
  • Pickle is not the only carrier. JFrog's set included Keras models as well as PyTorch ones [3].

Scan anyway: it catches commodity samples and leaves a record. Just do not let the scanner be the control that has to be right. The one that does not depend on it is to stop executing the format.

Didn't PyTorch 2.6 fix it?

It closed the most direct path. PyTorch 2.6.0, released in January 2025, made torch.load use weights_only=True by default, a change its release notes list as backwards incompatible [5]. The unpickler is then restricted to tensors, primitive types, dictionaries and types explicitly allowed through torch.serialization.add_safe_globals() [6], so a checkpoint that names os.system no longer gets it called.

The gaps are specific. Code pinned below 2.6 keeps the old default. Checkpoints that serialize a whole nn.Module instead of a state dictionary fail under the new default, and the usual workaround, weights_only=False, restores full pickle behavior for that call. The documentation still says, in bold, never to load data from an untrusted source [6]. And joblib, widely used for scikit-learn models, is pickle-based with no equivalent restriction.

Common model loading paths and whether loading can run code
Loading pathCan loading run arbitrary code?What to do
pickle.load on a .pkl fileYes; the unpickler imports and calls what the file names [1]Never on untrusted files; convert in an isolated job or reject
torch.load with weights_only=FalseYes; full pickle behavior [6]Treat as code execution; allow only for files you produced
torch.load with weights_only=True (default from 2.6)Restricted to tensors, primitives, dicts and allowlisted types [5][6]Use it, keep add_safe_globals lists short, and still isolate untrusted files
joblib.load on a scikit-learn modelYes; the format is pickle-basedSame handling as raw pickle
7z-compressed or deliberately broken PyTorch fileThe payload can run before the loader fails, and scanners may miss it [4]Reject archives that do not match the expected container
safetensors loadNo code step; the format stores tensors [7]Preferred format for anything entering production

What would a compromise look like from the inside?

Quietly ordinary. Picture an illustrative health-tech company, Sorrel Health, whose engineer evaluates a fine-tuned clinical-notes checkpoint from a month-old hub account on a shared GPU host with outbound internet and a cloud role that can read a training-data bucket. The checkpoint is a full serialized module, so the engineer adds weights_only=False; the load starts a background process that takes the host's cloud credentials from the instance metadata endpoint, posts them to a remote server and exits. The model then evaluates reasonably, and the blast radius is everything that role can reach, including training data that may contain patient records. Every signal was upstream: an unapproved source, a pickle format, a disabled safe loader and a host allowed to talk to the internet.

What should model intake look like?

One route in, run by a service with access to nothing valuable, producing a converted and pinned artifact that is the only thing other systems may load.

A five-stage intake pipeline for modelsFig. 2
THIRD-PARTY MODEL → PRODUCTIONApproved sourcenamed orgs onlyScanpickle opcodesConvertto safetensorsPin + signhash recordedIsolated loadno egressA FILE THAT FAILS ANY STAGE NEVER REACHES A HOST WITH CREDENTIALSscanning narrows the risk; conversion and isolation remove the code path

Models enter only from approved sources, are scanned for pickle opcodes, converted to safetensors, pinned and signed by hash, and loaded in isolation with no egress. Each stage removes a way the file could run code in a place that matters.

  • Approved source. Allow downloads only from named organizations or accounts you have reviewed, through a proxy or mirror, and block direct hub access from training and serving hosts. Hugging Face recommends loading from users and organizations you trust and relying on signed commits, which establish origin but not safety [2].
  • Scan. Disassemble pickle content without executing it, as Hugging Face's own scanner does with pickletools, and reject any import outside a short allowlist of tensor-rebuilding functions instead of hunting for known-bad ones [2]. Reject containers that do not match the expected format.
  • Convert. Rebuild the weights as safetensors in a disposable sandbox with no network and no credentials, using weights_only=True. Conversion has to read the original, so this is the one place an untrusted pickle is opened, and it must be a place where code execution gains nothing [6][7].
  • Pin and sign. Record the SHA-256 of the converted file, sign it in your artifact store, and make every consumer verify it. A model reference in code should be a digest, not a repository name and a branch.
  • Isolated load. Serve and evaluate on hosts with outbound network denied by default, no instance-metadata access unless required, and the narrowest cloud role that works.

The egress rule carries more weight than it looks. Both published payloads were reverse shells to hard-coded addresses [3][4]. A serving host that can reach only its artifact store and its callers turns a successful payload into a failed connection and an alert, which is a far better day than a quiet credential theft.

Where does Intertrace fit?

Not at intake: Intertrace does not scan model files, and the pipeline above is yours to run. The gateway sits on the other side of the model. Inference and service calls through it carry an itr_rt_* runtime key, so tenancy comes from the key, every decision is a structured event you can export to your SIEM, and the enforcer fails closed when it cannot decide. Discovery's read-only scans of cloud accounts and GitHub repositories surface AI services and AI usage in code and CI, which helps you find the hosts and pipelines that load models before deciding which need the egress rules.

What do we check first?

Search your code for torch.load with weights_only=False, for pickle.load and joblib.load on downloaded files, and for PyTorch pinned below 2.6. Each hit is a code-execution call site to justify or remove. Then, from one model-serving host, try to reach an arbitrary internet address and the instance metadata endpoint. If either succeeds without a written reason, that host is where a payload like goober2's would have phoned home, and it is the first one to fix. After that, build a harmless test pickle whose reduce step writes a marker file, and confirm the marker never appears anywhere except the conversion sandbox.

Sources

Reporting, research and specifications this piece draws on. Links go to third-party sources; analysis and recommendations are Intertrace’s own.

  1. 01pickle: Python object serialization · Python documentation
  2. 02Pickle Scanning · Hugging Face Hub documentation
  3. 03Data Scientists Targeted by Malicious Hugging Face ML Models with Silent Backdoor · JFrog
  4. 04Malicious ML models discovered on Hugging Face platform (nullifAI) · ReversingLabs
  5. 05PyTorch 2.6.0 release notes · PyTorch (GitHub)
  6. 06torch.load · PyTorch documentation
  7. 07Safetensors · Hugging Face

About this desk

Intertrace Threat Research

Threat research

How attacks on AI agents work in practice, from public incidents and our own test corpora, and the controls that stop them.

All 18 pieces by Intertrace Threat Research

Dated August 27, 2026 · added to intertrace.ai October 19, 2026

+ Runtime enforcement

See these controls on your own agent traffic.

Intertrace sits inline between your agents and the models, tools and MCP servers they call, and decides every call before it runs.

Keep reading

All insights →