Model File Format Security: Pickle, Safetensors, GGUF
Which model formats can execute code when loaded, which only crash, and which are inert. A format-by-format comparison built from the verified CVE record.
“Is this model file safe to load?” is the wrong question, because the answer depends almost entirely on the format rather than the file. Some formats hand the loader a program. Some hand it a parser problem. One hands it a length-prefixed byte range and nothing else. Choosing the format is the highest-leverage decision available, and it is made once per pipeline rather than once per artifact.
What follows is a format-by-format comparison built from published CVE records, not from vendor positioning. The pattern that emerges is not “old formats bad, new formats good”. It is that each format fails in a characteristic way, and the failure moves as the format gets safer — from the payload, to the parser, to the metadata that points at other files.
The comparison
| Format | What the loader does with it | Code execution by design? | Characteristic CVE class |
|---|---|---|---|
pickle (.pt, .bin, .pkl, joblib) | Reconstructs arbitrary Python objects, calling constructors | Yes | RCE on load, unfixable at the format level |
.keras / .h5 (Keras 3) | Rebuilds a layer graph from a JSON config, including callables | Yes, gated by safe_mode | Repeated safe_mode bypasses |
| GGUF | Parses a binary header and tensor table in C/C++ | No | Memory-safety bugs in the parser |
| ONNX | Parses protobuf, then resolves external_data file references | No | Path traversal in the loader library |
| safetensors | Reads a JSON header, then maps raw tensor bytes | No | Almost none in the format; risk moved to the index |
Read the last column top to bottom and you have a hierarchy of how much can go wrong, not a binary of safe versus unsafe.
pickle: the format that is a program
Pickle is a stack-based virtual machine. Opcodes in the stream name a callable and its arguments, and unpickling runs them. Loading is execution, and no flag changes that. The definitive entry remains CVE-2025-32434, where torch.load was exploitable even with weights_only=True on PyTorch 2.5.1 and earlier — proof that a guard bolted onto an unsafe loader is a guard, not a boundary.
The class recurs anywhere the default is missed. CVE-2025-24357 records vLLM’s hf_model_weights_iterator calling torch.load with weights_only left at its default of False. CVE-2026-12484 (7.8) records Keras 3.15.0 deserializing attacker-controlled PyTorch pickle data through the public TorchModuleWrapper.from_config method. MLflow’s flavour loaders produced ten simultaneous advisories of the same shape, covered in why MLflow 2.14.1 still fails your scanner.
The mechanics, including why __reduce__ makes scanning a heuristic rather than a proof, are in the pickle problem behind ML CVEs.
Keras .keras and .h5: a mitigation that keeps getting bypassed
Keras 3 introduced safe_mode=True to block the Lambda-layer code path that made older Keras archives directly executable (CVE-2024-3660, 9.8). The record since then is the clearest available demonstration of the difference between a filter and a boundary:
- CVE-2025-1550 (9.8) — arbitrary code execution even with
safe_mode=True, by alteringconfig.jsoninside a crafted.kerasarchive. - CVE-2025-8747 (7.8) — a safe-mode bypass in
Model.load_modelacross Keras 3.0.0 through 3.10.0. - CVE-2025-9905 and CVE-2025-9906 (7.3 each) — the same outcome through a crafted
.h5/.hdf5archive and through a crafted.kerasarchive respectively, both withsafe_mode=True. - CVE-2025-49655 (9.8) — deserialization of untrusted data in 3.11.0 up to 3.11.3 via an uploaded file containing a
TorchModuleWrapper. - CVE-2026-12481 (9.8) — in 3.14.0,
_raise_for_lambda_deserialization()fails to prevent Lambda-layer code execution, reopening the original 2024 hole.
Six independent bypasses of one mitigation in the sixteen months from March 2025 to July 2026 is not a run of bad luck; it is what happens when the format’s job is to reconstruct arbitrary callables and the mitigation’s job is to guess which ones are dangerous. Keras also carries a parallel file-handling cluster — path traversal in keras.utils.get_file tar extraction (CVE-2025-12060, CVE-2025-12638, CVE-2026-11816) and arbitrary HDF5 file reads (CVE-2026-1669, and CVE-2026-12480 for the incomplete fix). More on the TensorFlow-side context in TensorFlow security vulnerabilities.
Treat safe_mode=True as a speed bump for accidental damage, never as a control that lets you load untrusted .keras files.
GGUF: no execution by design, plenty of parser
GGUF is a header, a key-value metadata block, and a tensor table. Nothing in it names a callable, so there is no code-execution-by-design problem. What there is instead is a hand-written C/C++ parser reading attacker-controlled lengths, counts and offsets.
llama.cpp’s 2024 disclosure batch put five heap-based buffer overflows in five different header fields at 9.8 apiece (CVE-2024-21802, CVE-2024-21825, CVE-2024-21836, CVE-2024-23496, CVE-2024-23605), each described as capable of leading to code execution. The class is still open: CVE-2025-53630 (8.9) is an integer overflow in gguf_init_from_file_impl producing heap out-of-bounds read and write, and CVE-2026-7482 (9.1) is an Ollama-side heap out-of-bounds read where declared tensor offsets exceed the real file length.
So the honest framing for GGUF is: safer than pickle because the format grants no execution primitive, but not inert, because the parser is C++ and the input is hostile. Runtime-side detail is in inference server CVEs.
ONNX: the format is fine, the loader dereferences paths
ONNX is protobuf. It describes a computation graph declaratively and does not carry code. Its entire CVE record is about the loader following file references it should not.
The external_data field of a tensor proto can point outside the model directory: CVE-2022-25882 and CVE-2024-27318 (7.5 each) are directory traversal on read; CVE-2025-51480 (8.8) is arbitrary file write through traversal sequences in external_data.location during save_external_data. The download helpers had their own pair — CVE-2024-5187 (8.8) and CVE-2024-7776 (9.1), both arbitrary file overwrite through path traversal in a malicious model archive.
The pattern continued into 2026 with CVE-2026-34445 (8.6, ExternalDataInfo using setattr() to load metadata), CVE-2026-28500 (9.1, a security-control bypass in onnx.hub.load()) and CVE-2026-34446, where the symlink check meant to prevent traversal was incomplete.
If you ship ONNX, the format choice already removed the code-execution sink. What remains is to keep onnx current and to load models from a directory the process cannot escape.
safetensors: the format with almost no record of its own
safetensors is a JSON header giving each tensor a dtype, shape and byte range, followed by the raw bytes. There is no opcode, no callable, no path. The loader’s job is bounds-checking a slice.
The checkable evidence for this is the absence, not a claim: as of 18 August 2026 an NVD keyword search for “safetensors” returns a single result, and it is not in the format parser at all. CVE-2026-65920 (4.3) is a path traversal in Diffusers’ _get_checkpoint_shard_files, where malicious weight_map values in the model index JSON allow arbitrary file reads.
That single entry is the important one, because it shows where the risk goes once the payload is inert. It goes to the index: the model.safetensors.index.json that says which shard holds which tensor, the config.json that says which class to instantiate, and the repo metadata that says which files to fetch. Same shape as ONNX external_data, same shape as the Keras archive config. A format that cannot execute code still ships a manifest that tells the loader what to open.
What a safe format does and does not buy you
Switching weights to safetensors removes the deserialization sink. It does not remove:
- Auxiliary files.
config.json, tokenizer files, chat templates and custom code in the repo are loaded alongside the weights.trust_remote_coderemains a full code-execution decision independent of weight format — see trust_remote_code and the ML orchestration CVE class. - Index and shard resolution. CVE-2026-65920 is exactly this, and it is the pattern to expect more of.
- Provenance. An inert file with the wrong weights in it is still a supply-chain problem. Pull by digest and record the hash that was scanned. The repository-level risks are covered in Hugging Face model supply chain risk.
- Model behaviour. Backdoored weights are a training-data and evaluation problem, not a serialization one.
A workable policy
- Default to safetensors for weights. It is the only format here that removes the execution primitive rather than guarding it.
- Treat pickle-family artifacts as executables. If one must be loaded, do it in a sandbox that has no credentials and no network, then re-serialize to safetensors and discard the original.
- Never rely on
safe_mode=Truefor untrusted Keras archives. Six documented bypasses say the mitigation is best-effort. - Keep the loader library current even for inert formats. ONNX and GGUF risk lives in the parser, so version currency is the whole control.
- Validate the index, not only the payload. Reject any path in a shard index,
external_data.locationor archive member that escapes the model directory. - Scan in CI, and know the limits.
picklescan,ficklingand ModelScan flag dangerous opcodes but have documented bypasses; scanning is defence in depth. Tool selection is compared in ML vulnerability scanner software and detection workflow in malicious model file detection.
To check the current advisory state of the specific loader libraries in your stack — torch, keras, onnx, transformers — filtered by severity and fix availability, use the ML stack CVE filter. There is no safetensors chip in it, which is the point: after the August 2026 re-verification pass against NVD there was no published safetensors entry left to list.
See also
Sources
- CVE-2025-1550 — Keras Model.load_model arbitrary code execution with safe_mode=True (NVD)
- CVE-2025-51480 — ONNX save_external_data path traversal arbitrary file write (NVD)
- CVE-2026-65920 — Diffusers shard-file path traversal via weight_map (NVD)
- Pickle scanning and model serialization security — Hugging Face Hub docs
ML CVEs — in your inbox
CVEs in ML libraries, frameworks, and the AI/ML supply chain. Sent only when there is something worth sending.
No spam. Unsubscribe anytime.
Related
Malicious Model File Detection: Auditing ML Models
Pickle serialization flaws, PickleScan bypass CVEs, and a practical detection stack for teams pulling models from public repositories like Hugging Face.
Hugging Face Model Supply Chain Risk: Pickle Backdoors
How Hugging Face model supply chain risk works: pickle backdoors, the Transformers RCE CVE cluster, why the Hub scanner misses them, and what cuts risk.
TensorFlow Security Vulnerabilities 2026: CVEs and Supply Chain
A breakdown of the top TensorFlow security vulnerabilities in 2026: CVE-2025-49655, CVE-2025-12058, DoS flaws in 2.18.0, and supply chain risk.