ML CVEs
Flat isometric illustration of a gold padlock on an orange pedestal, ringed by orange hexagonal blocks and discs joined by dashed lines on navy.
Vulnerability Tracking

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.

By ML CVEs Editorial · · 7 min read

“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

FormatWhat the loader does with itCode execution by design?Characteristic CVE class
pickle (.pt, .bin, .pkl, joblib)Reconstructs arbitrary Python objects, calling constructorsYesRCE on load, unfixable at the format level
.keras / .h5 (Keras 3)Rebuilds a layer graph from a JSON config, including callablesYes, gated by safe_modeRepeated safe_mode bypasses
GGUFParses a binary header and tensor table in C/C++NoMemory-safety bugs in the parser
ONNXParses protobuf, then resolves external_data file referencesNoPath traversal in the loader library
safetensorsReads a JSON header, then maps raw tensor bytesNoAlmost 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 altering config.json inside a crafted .keras archive.
  • CVE-2025-8747 (7.8) — a safe-mode bypass in Model.load_model across 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/.hdf5 archive and through a crafted .keras archive respectively, both with safe_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_code remains 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

  1. Default to safetensors for weights. It is the only format here that removes the execution primitive rather than guarding it.
  2. 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.
  3. Never rely on safe_mode=True for untrusted Keras archives. Six documented bypasses say the mitigation is best-effort.
  4. Keep the loader library current even for inert formats. ONNX and GGUF risk lives in the parser, so version currency is the whole control.
  5. Validate the index, not only the payload. Reject any path in a shard index, external_data.location or archive member that escapes the model directory.
  6. Scan in CI, and know the limits. picklescan, fickling and 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

  1. CVE-2025-1550 — Keras Model.load_model arbitrary code execution with safe_mode=True (NVD)
  2. CVE-2025-51480 — ONNX save_external_data path traversal arbitrary file write (NVD)
  3. CVE-2026-65920 — Diffusers shard-file path traversal via weight_map (NVD)
  4. Pickle scanning and model serialization security — Hugging Face Hub docs
#model-security #serialization#safetensors #pickle #gguf#onnx#keras #ml-supply-chain
Subscribe

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