ML CVEs
Software code on a developer's screen
Vulnerability Tracking

Unsafe Model Deserialization: The Pickle Problem Behind ML CVEs

Loading a model file can execute arbitrary code. This is the single most repeated vulnerability class in the ML supply chain — the real CVEs, why the fixes keep arriving late, and what actually mitigates it.

By Marcus Reyes · · 8 min read

Almost every serious vulnerability in the model-distribution supply chain reduces to the same root cause: a deserialization routine that executes code while it is reconstructing an object. It is CWE-502, it has been understood for decades, and it keeps producing ML CVEs because the serialization format the ecosystem standardized on was never safe to point at untrusted data — and the entire model-sharing culture is built on pointing it at untrusted data.

Why the default serialization primitive is the wrong one for model files

The Python documentation states the problem plainly: the relevant module is not secure, and you should only deserialize data you trust. Deserialization here is not a parser; it is a small stack-based virtual machine. A REDUCE opcode calls a callable with arguments taken from the byte stream, and a malicious object’s reduction method can return something equivalent to (os.system, ("command",)). Loading such a file runs the command. No memory corruption, no exploit chain — the format does exactly what it was designed to do.

PyTorch’s default .pt/.pth checkpoints, older joblib and scikit-learn artifacts, and many community model dumps use this format under the hood. When a practitioner downloads a checkpoint from a random repository and calls torch.load(path), they are running whatever the producer embedded in it. Trail of Bits demonstrated full weaponization of this in 2021, including payloads that survive re-serialization, and the technique has not aged.

The CVEs keep arriving — even after the “fix”

The instructive recent example is CVE-2025-32434. PyTorch had long recommended torch.load(..., weights_only=True) as the safe path, and a great deal of guidance treated that flag as the mitigation. CVE-2025-32434 showed that on PyTorch prior to 2.6.0, weights_only=True could still be bypassed to achieve remote code execution. The control everyone was told to rely on did not fully hold; the real fix was an upgrade plus making weights_only=True the default in 2.6.0.

This is the pattern worth internalizing: a deserialization mitigation that is merely a flag on a fundamentally unsafe loader is one parser bug away from failing, and the CVE that proves it tends to arrive years after the advice solidified. The same shape recurs across the ecosystem. Keras .h5/SavedModel files have carried code-execution issues through Lambda layers and deserialized configs. numpy.load with allow_pickle=True is a CWE-502 sink and was the basis of CVE-2019-6446. Each is independently scored, each gets its own NVD entry, and a defender tracking only “PyTorch CVEs” or only “numpy CVEs” sees fragments of one structural problem.

Reading these CVEs correctly

A deserialization CVE in an ML library should be triaged against one question: does my pipeline ever load a model artifact whose bytes I did not produce and verify? If the answer is yes — fine-tuning community checkpoints, loading user-uploaded models, pulling from a public hub at runtime — the practical severity is closer to unauthenticated RCE on the inference host than whatever generic CVSS vector the entry carries. If every artifact is built in-house from trusted source and stored in an integrity-checked registry, the same CVE may be largely inert for you. The score will not tell you which case you are in; your architecture does.

What actually mitigates it

  • Change the format, not the flag. Prefer safetensors for weights. It is a non-executable format: a header plus raw tensor bytes, with no callable invocation during load. This removes the sink rather than guarding it.
  • Treat model files as untrusted code by default. Scan artifacts before loading. Hugging Face runs scanning on the Hub and surfaces results; picklescan and fickling (Trail of Bits) can flag dangerous opcodes in CI before a file ever reaches torch.load.
  • Pin and verify provenance. Pull models by digest, not floating tags. Record the hash that was scanned and reject anything that does not match at load time. Content-addressed registry storage makes “did these bytes change” answerable.
  • Sandbox the loader. If you must load legacy artifacts in the unsafe format, do it in a locked-down, network-isolated process with a minimal filesystem view, so a REDUCE payload lands somewhere disposable.
  • Keep loaders current and re-test assumptions. CVE-2025-32434 is the reminder that “we set weights_only=True” is a claim with an expiry date. Track the loader’s CVEs specifically and upgrade, rather than trusting a flag whose guarantees a future parser bug can revoke.

The deserialization CVE is not going away, because the incentive that created it — frictionless model sharing — is not going away. The defenders who handle it well stop treating each entry as a new surprise and start treating “we deserialize artifacts we did not produce” as a standing exposure that the next CVE merely re-confirms.

Sources

  1. CWE-502: Deserialization of Untrusted Data — MITRE
  2. Python pickle module — security note (official docs)
  3. Never a dill moment: exploiting ML pickle files — Trail of Bits
  4. Pickle scanning and safetensors — Hugging Face
  5. CVE-2025-32434: PyTorch torch.load RCE despite weights_only — NVD
Subscribe

ML CVEs — in your inbox

CVEs in ML libraries, frameworks, and the AI/ML supply chain. — delivered when there's something worth your inbox.

No spam. Unsubscribe anytime.

Related

Comments