Loading a model file is often the same as running code, and most teams still download model weights from public hubs with less scrutiny than they give an npm package. The default PyTorch format for years was pickle, which can import and call arbitrary Python during deserialisation, and researchers have found malicious models on public hubs that open a remote shell the moment they are loaded. This guide covers the model artefact itself: which file formats can execute code, how safe loading options in PyTorch, Keras and Transformers work and where they stop, what hub scanning catches, and how to build an intake pipeline with pinning, signing and an internal registry. It is for ML platform, application security and supply chain teams. Inventory and AI-BOM practice is covered in the AI Supply Chain and AI-BOM Guide, and behavioural backdoors in weights in the AI Data Poisoning and Backdoors Guide.
Key takeaways
- Pickle is code, not data. Python's own documentation says the pickle module is not secure and that malicious pickle data can execute arbitrary code during unpickling.
- Safetensors removes the execution path for weights. It stores tensors only, so prefer it for every model you download or publish, and convert legacy checkpoints in an isolated job.
- Safe loading flags help but are not a sandbox. PyTorch 2.6 and later default to
weights_only=True, and Keras defaults tosafe_mode=True, yet both have documented limits and both can be switched off by a single argument. - Hub scanning is advisory. Hugging Face scans every commit and highlights suspicious pickle imports, but states its scan is not foolproof, and flagged files can still be downloaded.
- Provenance needs pinning and signatures. Pin every production model to a commit hash and a file digest, mirror it into an internal registry, and verify signatures where the publisher provides them.
What sits in a model supply chain
A model you deploy is a bundle of artefacts from several parties, each of which can be tampered with. OWASP's LLM03:2025 Supply Chain entry lists vulnerable pre-trained models, weak model provenance (model cards give no guarantee of origin), malicious LoRA adapters, collaborative model merge and conversion services, and traditional package vulnerabilities as distinct risks.
| Artefact | Typical files | How it can hurt you | Primary control |
|---|---|---|---|
| Weights | pytorch_model.bin, .pt, .pth, .ckpt, .safetensors, .keras, .h5 | Code execution on load (pickle, Keras Lambda); tampered parameters | Code-free formats, safe loading, digest pinning |
| Modelling code | Python files loaded with trust_remote_code=True | Arbitrary code by design, changes with every commit | Review and pin, or disallow |
| Adapters and merges | LoRA and PEFT adapter weights, merged checkpoints | Malicious adapter alters a trusted base model (OWASP LLM03) | Same intake as base weights |
| Tokenisers and configs | tokenizer.json, config.json, generation configs | Altered behaviour; usually data, but loaders vary | Digest pinning |
| Libraries | PyTorch, Transformers, serving frameworks, converters | Classic dependency compromise | Software supply chain controls |
| Publisher account | Hub user or organisation, its tokens | Account takeover replaces a trusted model | Pinning; publisher token hygiene |
Classic dependency risk is covered in the Software Supply Chain Security Guide; the rest of this page deals with the model-specific rows.
Why pickle-based model files execute code
Pickle is a stream of opcodes for a small virtual machine, not a passive data layout. Hugging Face's pickle documentation identifies the opcodes that matter: GLOBAL and STACK_GLOBAL import an object by module and name, and REDUCE calls it with arguments. An attacker who controls the file can therefore import a built-in that runs code and call it while the rest of the file still looks like ordinary weights. The same mechanism underlies insecure deserialisation in any language; see the Insecure Deserialisation Guide.
- It was the default: PyTorch checkpoints saved with
torch.save(pytorch_model.bin,.pt) are pickle inside a zip, so a large share of older Hub models are pickle. - It has been abused on public hubs: JFrog reported in February 2024 that it had found around 100 malicious models on Hugging Face, mostly PyTorch and some Keras, including one whose payload opened a reverse shell to a remote host when loaded with
torch.load(). - Allowlisting the unpickler is hard: Python documents overriding
Unpickler.find_class()to forbid or restrict globals, but warns you must be careful what you allow. Hugging Face notes a custom unpickler alone is not a sufficient fix, citing a CTF bypass. - Other formats execute code too: Protect AI's Hub scanner looks for Keras Lambda layer exploits as well as pickle, and Keras documents that loading with
safe_mode=Falsecan trigger arbitrary code execution.
Model file formats compared
| Format | Can execute code on load? | Notes | Policy |
|---|---|---|---|
Pickle checkpoints (.bin, .pt, .pth, .ckpt, .pkl) | Yes, unless loaded with a restricted unpickler | Default PyTorch format before safetensors; weights_only=True restricts it | Block from untrusted sources; convert in isolation |
Safetensors (.safetensors) | No code path: tensors only | Zero-copy and lazy access with safe_open and get_slice; supported by Transformers, Diffusers, llama.cpp and others | Preferred default |
Keras v3 (.keras) | Only if safe_mode=False or custom objects are registered | Zip of config, weights and optimiser state; safe_mode blocks unsafe lambda deserialisation | Allow with safe_mode=True; review custom objects |
Legacy Keras HDF5 (.h5) | safe_mode applies only to Keras v3 format | Keras documents the flag for .keras only | Treat as untrusted code; convert |
Modelling code via trust_remote_code | Yes, by design | Transformers requires the flag to load custom architectures from a repository | Disallow by default; review and pin by exception |
Hugging Face's pickle guidance also suggests loading TensorFlow or Flax checkpoints into PyTorch with from_tf=True or from_flax=True where a repository offers them, and lists MsgPack, Protobuf, Cap'n Proto and Avro as alternative serialisation formats.
Safe loading controls and their limits
PyTorch
- Default since 2.6:
torch.loadusesweights_only=Truewhen nopickle_moduleis passed. The restricted unpickler only builds plain tensors and some primitive types needed forstate_dicts and cannot import anything dynamically. - Documented limits: PyTorch states it does not protect against denial of service, that memory corruption is mitigated but may still be possible, and that loaded objects can still cause problems in downstream code.
- Allowlisting:
torch.serialization.add_safe_globals()adds globals process-wide, while thesafe_globals()context manager limits the allowlist to one block; prefer the context manager.get_unsafe_globals_in_checkpoint()lists non-allowlisted globals by static analysis, and PyTorch notes it may miss types built dynamically. - Environment overrides:
TORCH_FORCE_WEIGHTS_ONLY_LOAD=1forces every call toweights_only=True; set it in serving images.TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1does the opposite for calls that do not pass the argument; alert if it appears in any image or job definition. - The escape hatch: any code calling
torch.load(..., weights_only=False)restores full pickle behaviour, and PyTorch's own error message warns this can lead to arbitrary code execution. Make it a code review and static analysis finding.
Keras, Transformers and plain Python
- Keras:
keras.saving.load_model(..., safe_mode=True)is the default and disallows unsafe lambda deserialisation for.kerasfiles. Search code forsafe_mode=False. - Transformers:
trust_remote_code=Trueloads Python from the model repository. Treat each use as a third-party code dependency: review the files, pin the repository to a commit, and record the exception. - Plain pickle and joblib: scikit-learn and other classic ML models are often pickled. Python's advice is never to unpickle data that could have come from an untrusted source or been tampered with, and to consider
hmacsigning where you control both ends.
Choosing between controls
- You control the publisher: publish safetensors only and sign it; consumers then need no unpickling at all.
- Third-party model only in pickle: convert once in an isolated, network-less job, scan the result, publish the safetensors copy internally, and never load the original in production.
- Old checkpoints that store a whole
nn.Module: PyTorch recommendsweights_only=Falsefor these, so they should only ever come from your own trusted training runs. Re-save them as astate_dictto retire the exception. - Small teams: the cheapest effective control is a rule that production loads only safetensors from an internal mirror. Large organisations add an intake service with scanning, signing and an approval workflow.
Scanning and its limits
Scanning is a useful tripwire and a poor guarantee. Static scanners read the pickle opcode stream without executing it and flag imports that look dangerous; they can be evaded by imports not on the list, and they say nothing about behavioural backdoors in weights.
| Layer | What it does | What it misses |
|---|---|---|
| Hub malware scan | Scans files under 2 GB at each commit; ok or infected badge; repository warning | Antivirus does not detect pickle exploits, per Hugging Face |
| Hub pickle import scan | Lists imports using pickletools.genops without executing; highlights suspicious ones | Best-effort safe and unsafe lists; Hugging Face says it is not 100% foolproof |
| Third-party Hub scanners | Protect AI Guardian and others add per-file reports on public repositories | Private repositories and files outside the Hub; download is not blocked |
| Your intake scanner | Static pickle analysis, format allowlist, digest check, before mirroring | Novel import chains; weight-level backdoors |
| Isolated load test | Load in a sandbox with no network and no credentials, watch for process and network activity | Triggers that depend on environment or time |
| Behavioural evaluation and red teaming | OWASP LLM03 recommends AI red teaming of third-party models for your use case | Narrow triggers designed to pass benchmarks |
Provenance, signing and pinning
- Pin to a commit, not a name: loading
org/modelwithout a revision follows the repository's default branch, so a compromised publisher token changes what you load. Pin a commit hash in code and in the AI-BOM. - Record file digests: store SHA-256 digests of each approved file and verify them at load time; D3FEND's File Hashing technique describes the control.
- Verify signatures where available: the OpenSSF-associated
model_signingproject (sigstore/model-transparency) signs a model directory with Sigstore, keys, certificates or PKCS #11 hardware, produces a Sigstore bundle holding an in-toto statement of file digests, and on verify recomputes the hashes. With Sigstore, signing events land in a transparency log publishers can monitor. - Signed commits are not signed models: Hugging Face notes that GPG-signed commits guarantee origin, not safety.
- Protect your own publishing identity: publish from CI with short-lived Trusted Publisher tokens rather than a stored write token, so a leaked CI secret cannot replace your models. See the Hugging Face Security Guide and the AI Model Provenance and Watermarking Guide.
Building a model intake pipeline
- Inventory what you load today: grep code and notebooks for
from_pretrained,torch.load,pickle.load,joblib.load,load_modelandtrust_remote_code; record source, revision and format in the AI-BOM. - Set the format policy: safetensors and Keras v3 with
safe_mode=Trueallowed; pickle formats and remote code only by documented exception. - Stand up an internal mirror: a model registry or artefact store that production can reach, with production egress to public hubs blocked.
- Intake job: download into an isolated runner with no credentials and no network after download, check format, run static scans, convert pickle to safetensors, compute digests, sign, and publish to the mirror.
- Audit mode first: for two to four weeks log every load that would be blocked (wrong format, unpinned revision, unsigned file) without blocking, and fix the top offenders.
- Enforce in rings: set
TORCH_FORCE_WEIGHTS_ONLY_LOAD=1and mirror-only loading in staging, then one production service, then all. Rollback is reverting the image variable or the egress rule for the affected service, with an exception ticket. - Continuous checks: re-scan mirrored models when scanner rules update, and review exceptions quarterly.
Detection and measurement
- Load-time logging: log model identifier, revision, file digest and loader arguments for every model load in serving and training jobs.
- Runtime signals: alert on child processes (shells, interpreters) or new outbound connections from model-serving and notebook processes during load, which maps to ATT&CK T1059.006 Python and ATLAS AML.T0011.000 Unsafe AI Artifacts.
- Code and config signals: CI rules that fail on
weights_only=False,safe_mode=False, unreviewedtrust_remote_code=TrueandTORCH_FORCE_NO_WEIGHTS_ONLY_LOAD.
Metrics:
- Percentage of production model loads from the internal mirror.
- Percentage of production models in safetensors or another code-free format.
- Percentage of model references pinned to a commit hash with a recorded digest.
- Number of open
trust_remote_codeand pickle exceptions, and their age. - Median time from a scanner alert on a mirrored model to quarantine.
Framework mapping
| Threat or control | MITRE ATLAS / ATT&CK | NIST CSF 2.0 | NIST SP 800-53 Rev. 5 |
|---|---|---|---|
| Malicious model on a public hub | AML.T0058 Publish Poisoned Models; AML.T0010.003 AI Supply Chain Compromise: Model | ID.RA-09 | SR-4 Provenance, SR-11 Component Authenticity |
| Code execution when loading | AML.T0011.000 Unsafe AI Artifacts; T1059.006 Python | PR.PS-05 | SI-3, CM-7 |
| Compromised AI libraries | AML.T0010.001 AI Software; T1195.002 Compromise Software Supply Chain | GV.SC-07 | SR-3, SR-6, RA-5 |
| Tampered weights | AML.T0018 Manipulate AI Model | ID.RA-09 | SI-7 |
| Integrity verification | AML.M0014 Verify AI Artifacts; AML.M0013 Code Signing; D3FEND D3-FH File Hashing | ID.RA-09 | SI-7, SR-4 |
| Model inventory | AML.M0016 Vulnerability Scanning | ID.AM-02, ID.AM-08 | CM-8 |
Common failure patterns
- Trusting the hub badge: a model without a warning is treated as safe, although Hugging Face says its scan is not foolproof and JFrog showed flagged models could still be downloaded.
- Silencing the error: a developer hits a
weights_onlyunpickling error after a PyTorch upgrade and addsweights_only=False, reopening the code execution path for every checkpoint that code loads. - Remote code left on:
trust_remote_code=Truecopied from a model card into production, unpinned, so the next commit to that repository runs in your serving environment. - Loading by name in production: no revision pin and no digest, so a publisher account takeover becomes your incident.
- Notebooks as the soft spot: data scientists load experimental models on laptops or notebook servers that hold cloud credentials and Hub write tokens. Isolate experimentation from credentials. See the AI Coding Agents Security Guide for the same pattern with agents.
- Converting in place: running the pickle to safetensors conversion on a build host with secrets, which executes the very payload you were trying to remove.
Practitioner checklist
- Inventory every model load and record source, revision, format and digest in the AI-BOM.
- Make safetensors the default format for models you publish and consume.
- Set
TORCH_FORCE_WEIGHTS_ONLY_LOAD=1in serving images and fail CI onweights_only=Falseandsafe_mode=False. - Disallow
trust_remote_codeexcept reviewed, pinned exceptions. - Serve production only from an internal model mirror and block egress to public hubs.
- Run intake and conversion in an isolated runner with no credentials and no network.
- Verify digests at load and signatures where publishers sign with
model_signing. - Monitor model-serving processes for child processes and unexpected outbound connections.
- Publish your own models from CI with short-lived tokens, never a stored write token.
Standards and references
- OWASP: Top 10 for LLM Applications 2025 (LLM03 Supply Chain)
- MITRE ATLAS: AI Supply Chain Compromise
- Python Software Foundation: pickle, Python object serialization
- PyTorch: Serialization semantics
- Hugging Face: Safetensors
- Hugging Face: Pickle Scanning
- Sigstore: Model Transparency (model_signing)
- JFrog: Data Scientists Targeted by Malicious Hugging Face ML Models with Silent Backdoor
Related NHI Mgmt Group resources: AI Supply Chain and AI-BOM Guide · Insecure Deserialisation Guide · Open-Weight Model Security Guide · Hugging Face Security Guide · SLSA and Build Provenance Guide