In February 2024, JFrog Security Research discovered a PyTorch model file on Hugging Face. It came from a seemingly innocuous account named “baller423”.
Upon inspection, they found that it contained a backdoor.
This backdoor was established when a user loaded the model by using standard commands like torch.load() which used its pickle deserialisation format. Upon doing so, hidden code would execute and open an active reverse shell connection to an external, attacker-controlled IP address (210.117.212.93). The attacker would then have full access to an interactive command shell with the privileges of the process that loaded the model.
All this would be done at loading with no inference needed in the process.
Why does this backdoor exist
Python’s pickle module serialises (i.e. turns objects into a flat stream of bytes or a text format) objects so they can be deserialised (i.e. reconstructed into objects) later.
Deserialising an object may require importing modules and calling functions, and to account for this a pickle file contains instructions.
Those instructions can be for things like rebuilding tensors and Python objects. They can also be for things like invoking os.system, starting a process, downloading a file or opening a network connection.
Python’s documentation does call out a caveat with this:
“The pickle module is not secure. Only unpickle data you trust.”
Traditional PyTorch checkpoints saved using torch.save() usually contain pickled metadata, commonly packaged in a ZIP archive file. Such files would generally use .pt or .pth extensions by convention.
If you download a model from a public repository, you should treat it as untrusted until you have verified its source, contents and loading path.
PyTorch is now secure by default in this regard
Since PyTorch 2.6, torch.load() uses weights_only=True by default when the caller has not supplied a custom pickle module. This invokes a restricted unpickler designed to load tensors, primitive types, dictionaries and explicitly approved classes. This prevents the unrestricted dynamic imports used by many conventional pickle exploits.
Note that it’s still possible for a project to require weights_only=False, add classes to the allowlist or use another loading path. Compatibility instructions sometimes instruct users to disable require weights_only when a checkpoint fails to load.
As a rule, you shouldn’t disable weights_only for an externally sourced model without understanding why you need to do so, and reviewing the model’s provenance and accompanying code.
Other reverse shell backdoors found
JFrog also identified about 100 models containing harmful payloads during their research. Approximately 95 per cent of its sample used PyTorch; the remainder used TensorFlow Keras.
Rapid7 documented another campaign in 2025. It turned out that attackers had uploaded several .pth files to Hugging Face. When deserialised unsafely, the files ran shell commands that downloaded and executed Linux ELF binaries. One payload contacted a VShell command-and-control endpoint concealed behind a Cloudflare Tunnel.
These were model files being used as software supply-chain delivery scenarios.
Scanning helps, but can’t make pickle trustworthy
Hugging Face scans uploaded repositories using several systems. This incluldes specialised pickle analysis, malware detection and commercial scanning services.
PickleScan examines pickle instructions and looks for globals and call patterns associated with unsafe behaviour. That can identify many malicious files. It remains a static analysis system operating against a language capable of importing modules and invoking functions.
Researchers have repeatedly found gaps in that coverage.
In 2025, ReversingLabs discovered two malicious Hugging Face models using a technique it named nullifAI. The files used 7z packaging instead of PyTorch’s normal ZIP layout, so they could not be loaded directly with the standard torch.load() path.
Inside each archive was a malformed pickle containing a reverse-shell payload near the beginning of its instruction stream. When the underlying pickle was deserialised, the payload executed before the loader encountered the malformed instruction later in the file.
The scanner rejected the file as broken without reporting the dangerous operation that appeared before the break.
Hugging Face removed the models, and PickleScan was subsequently changed to improve its handling of malformed pickle files.
While that closed one route, it did not address the underlying problem.
In March 2025, Sonatype disclosed four more PickleScan vulnerabilities. They included unsafe-function detection gaps, hidden files inside PyTorch archives and ZIP-header manipulations that PickleScan rejected even though PyTorch would still load the archive. The four issues were fixed in PickleScan 0.0.23.
Then, in July 2026, three researchers published a preprint called ShadowPickle.
They evaluated three attacks against ten model scanners and four hosting platforms. Their Overwritten Module technique achieved a 63 per cent evasion rate across the scanners they tested. The other techniques recorded rates of 42 and 41 per cent.
The Overwritten Module attack requires more than a malicious model file. Its threat model includes an attacker-controlled Python package capable of replacing a module trusted by the restricted loader. That matters when interpreting the result: the paper examines the model, its dependencies and the surrounding Python environment as one supply chain.
The paper has not yet been peer reviewed. Its percentages should be treated as experimental results under the authors’ test conditions.
The general conclusion is that static scanning reduces risk, but its result is limited by the formats, instructions and dependency behaviours the scanner understands.
A clean scan result means the scanner did not find an unsafe construct within its current coverage. Great for evidence but its not great as proof that loading the file is safe.
Make safetensors the default
Safetensors was created to store tensors without pickle’s executable instruction stream.
The format consists of a JSON header describing the tensors and a byte buffer containing their data. It has no pickle opcodes, object reconstruction hooks or facility for embedding arbitrary Python functions.
That removes the arbitrary-code-on-deserialisation mechanism described in this article.
It does not certify everything surrounding the model. A repository can still contain malicious Python, compromised dependencies or instructions telling the user to enable remote code. A model can also contain learned backdoors that affect its behaviour after loading. Safetensors addresses the model-weight serialisation problem.
A practical organisational standard would require:
- Safetensors for externally sourced model weights wherever the format is supported.
- PyTorch’s restricted weights_only loader when a PyTorch checkpoint must be used.
- Review and approval before anyone sets weights_only=False.
- trust_remote_code=False by default.
- Repository revisions pinned to a reviewed commit rather than a moving branch.
- Model files and dependencies retrieved through a controlled internal source.
- Conversion of unavoidable pickle checkpoints inside a disposable environment with no production credentials, no access to internal systems and tightly controlled network access.
Scanning should remain part of the process. It is valuable as a detection control. It should never be the permission mechanism that turns an untrusted pickle into an approved file.
Check the rest of your loading path
Model weights are only one part of the software entering a training environment.
The same environment may install packages from PyPI, download code from model repositories, pull containers from public registries and clone source from Git.
Hugging Face Transformers also supports trust_remote_code=True. When enabled, it allows custom Python supplied with a model repository to run locally. The name of the parameter is an accurate description of the trust decision being made.
AppSec teams already use controls that apply here: dependency pinning, provenance checks, artifact signing, curated mirrors, isolated builds and restricted execution identities.
The gap is usually scope. Model repositories and training platforms were introduced outside the processes that govern ordinary application dependencies.
As a next step, try asking your AppSec lead:
“Can anyone in this organisation load an external pickle checkpoint or enable remote repository code on a machine containing corporate credentials?”
If you don’t know, address that gap today.
Sources
- Python documentation: pickle security warning
- PyTorch documentation: serialization and weights_only
- JFrog: malicious Hugging Face models with a reverse-shell payload
- Rapid7: abuse of pickle files in AI model supply chains
- ReversingLabs: the nullifAI PickleScan bypass
- Sonatype: four PickleScan vulnerabilities
- Pradhan, Nambiar and Soremekun: ShadowPickle preprint
- Safetensors format and security rationale
