The CVE Record on Insecure Deserialization in AI Python Code

PyYAML's yaml.load carried a CVSS 9.8 CVE. PyTorch's torch.load carried one at 9.3, in 2025, in a flag documented as safe. Here is the actual track record.

Copper Sun Brass Team · · 9 min read
securityengineering

PyYAML’s yaml.load carried a CVE rated 9.8 out of 10. PyTorch’s torch.load carried one rated 9.3, in 2025, inside a parameter that PyTorch’s own documentation called the safe way to load a model. Insecure deserialization, tracked by MITRE as CWE-502, is not a hypothetical risk that AI code generation might someday introduce. It has a CVE history stretching back a decade, and the pattern keeps resurfacing one abstraction layer deeper each time a new ML framework ships its own version of “just load the file.”

This matters for anyone reviewing AI-generated Python because the failure mode is invisible at the call site. yaml.load(f) and yaml.safe_load(f) look identical in a diff — one word apart, wildly different security posture. torch.load(path, weights_only=True) reads like a safety flag was already applied. An AI assistant completing a config loader or a model-loading function has no reason to know which of these APIs carries a decade of CVE history behind it; the prompt asked for functionality, and both options satisfy it on safe input.

The Bug That Won’t Die: PyYAML’s yaml.load

BrassCoders treats CVE-2017-18342 as the reference case for why yaml.load is unsafe by default. PyYAML versions before 5.1 let yaml.load execute arbitrary code on crafted input — no authentication, no user interaction, exploitable over the network. The advisory rates it CVSS 9.8, near the ceiling of the scale.

The GitHub Security Advisory explains the mechanism, and it’s straightforward once you see it. PyYAML’s full Loader supports Python-specific tags like !!python/object/apply:subprocess.run, which construct and immediately call arbitrary Python objects during parsing. A YAML file is not just data to this loader; it’s a set of instructions for building objects, and one of those objects can be a running shell command. PyYAML’s fix, shipped in version 5.1 after an earlier attempt in 4.1 got rolled back for breaking compatibility, changed the library’s default Loader and pushed developers toward yaml.safe_load, which restricts parsing to plain Python types with no object construction. Eight years after the CVE, yaml.load with the unsafe Loader still shows up in freshly generated code, because the API itself never went away — only the recommendation changed.

The Same Bug, Three Layers Deeper: PyTorch’s torch.load

BrassCoders treats CVE-2025-32434 as proof that a documented safe mode can still ship a CWE-502 hole. PyTorch’s own documentation recommended torch.load(weights_only=True) as the way to load an untrusted checkpoint without risk. Rated CVSS 9.3, the vulnerability showed that flag alone did not stop remote code execution on PyTorch 2.5.1 and earlier.

The security advisory is worth sitting with for a moment. This wasn’t a case of a developer skipping a security flag out of ignorance. This was the flag PyTorch told everyone to use, in code that followed the documentation exactly, still carrying a critical vulnerability. The fix landed in PyTorch 2.6.0 — a version bump, not a code change on the caller’s side. A model-loading function generated by an AI assistant that correctly sets weights_only=True looks like defensive code. It is defensive code, against everything except the specific gap this CVE closed, on every PyTorch install before 2.6.0. Version pinning matters here in a way that a code reviewer scanning for the presence of a safety flag would miss entirely.

Even The Framework’s Convenience Wrapper Isn’t Safe

BrassCoders treats CVE-2026-12484 as evidence the deserialization risk moved into the ML framework’s own convenience wrappers, not just raw pickle calls. Keras’s TorchModuleWrapper.from_config method calls torch.load with weights_only=False by default, outside an explicit safe-mode context. Rated CVSS 7.8, it was patched in Keras 3.12.3 and 3.15.0.

The full advisory shows what’s absent from that vulnerable code path: the word “pickle.” Nobody writing or reviewing a call to TorchModuleWrapper.from_config sees a deserialization primitive at all. They see a Keras layer-loading API, three abstraction layers removed from pickle.load, which is itself the primitive torch.load wraps. This is the pattern that makes AI-generated ML code specifically risky compared to AI-generated general-purpose Python: the unsafe call gets buried inside framework glue that reads as routine, safe-looking model plumbing. A reviewer — human or AI — pattern-matching on the literal string pickle will miss every one of these.

Malicious Models Are Already In The Wild

BrassCoders treats JFrog’s Hugging Face research as the canonical evidence that pickle-based model deserialization is an active exploitation vector, not a theoretical one. JFrog’s security research team found close to 100 malicious models on Hugging Face carrying genuine harmful payloads, with PyTorch pickle files the most common carrier. One model, uploaded by an account the researchers named, opened a reverse shell to an attacker-controlled IP address the moment it was loaded.

The full writeup has a detail worth sitting with too. This wasn’t a synthetic test model or a proof-of-concept the researchers built themselves. It was a real file, uploaded to a real public repository, that a real data scientist could have downloaded and loaded into a training pipeline with a single torch.load call. An AI assistant asked to “write a script that downloads and loads the latest checkpoint from this Hugging Face repo” will produce exactly the call that triggers a payload like this one, because nothing in that prompt distinguishes a trusted checkpoint from a hostile one. The model file itself is the untrusted input, and it looks identical to a legitimate one until it’s already running.

What The Standard Library Already Warned You About

BrassCoders treats the pickle module’s own documentation as the most unimpeachable source in this category — the standard library warns about itself. The documentation states plainly: “The pickle module is not secure. Only unpickle data you trust.” It goes further, pointing to json for untrusted data or an hmac signature to verify data hasn’t been tampered with.

The pickle module docs carry that warning; the OWASP Deserialization Cheat Sheet names the same Python danger patterns explicitly — pickle, c_pickle, and PyYAML’s load method — and adds two general defenses that apply beyond Python: prefer a plain data format that can’t encode executable objects, or sign serialized data and reject anything unsigned before deserializing it. Neither source is buried in an obscure security blog. One ships inside the interpreter every Python developer already has installed. When an AI assistant writes pickle.load(untrusted_stream), it isn’t missing an edge case the industry hasn’t documented. It’s contradicting a warning that ships in the same standard-library page it presumably drew the function signature from.

What BrassCoders Catches — And What It Doesn’t

BrassCoders’ Bandit integration flags pickle.load, pickle.loads, and cPickle calls under rule B301, and unsafe yaml.load calls under rule B506, structurally, on every scan. The detection is pattern-based: any call matching the signature gets flagged, full stop, regardless of whether the surrounding code happens to be safe in this particular instance.

That’s a deliberate design choice, not a limitation BrassCoders is trying to hide. Deciding whether a specific pickle.load call actually receives untrusted input requires reading the surrounding code: where the bytes came from, whether they crossed a network boundary, whether a user or an external file supplied them. That’s context inference, and BrassCoders doesn’t do context inference. It reports the pattern honestly and leaves the “is this one real” judgment to whichever AI assistant reads the finding next, the same division of labor that governs every scanner BrassCoders bundles.

The model-file side of the problem is worth pairing with source-level scanning. ModelScan, from Protect AI, reads Pickle, SavedModel, and H5 model files byte-by-byte to flag unsafe code signatures without executing them — coverage for the file someone downloaded, complementary to BrassCoders’ coverage of the call site that loads it.

The ML-Specific Fix: Stop Deserializing Executable Code At All

BrassCoders treats the shift from pickle-based checkpoints to a byte-buffer format as the closest thing this category has to a structural fix rather than a patched flag. Hugging Face’s safetensors format stores tensors as raw byte buffers with a JSON header describing shape and dtype — no Python object graph, no __reduce__ method, nothing to execute. Loading a safetensors file cannot run code, by construction, the same guarantee yaml.safe_load gives you for YAML and json.loads gives you for arbitrary Python objects.

Safetensors is a different kind of fix than the four CVEs above required. PyYAML’s, PyTorch’s, and Keras’s fixes each closed one specific hole in a format designed to deserialize executable objects — the next hole in the same design is only a matter of time, which is exactly what happened three times in a row. Safetensors sidesteps the whole design. An AI assistant asked to “load a model checkpoint” has no strong reason to prefer one format over the other unless the prompt or the surrounding codebase steers it there; a requirements.txt pinned to a current framework version and a preference for .safetensors over .bin or .pt checkpoint files where the model publisher offers both closes more risk than either alone. Many popular Hugging Face repositories now publish both formats side by side. The safer one is often already sitting there, just not the default the older tutorial used.

Reproduce The Pattern Yourself

None of the four CVE-class findings above require special access to reproduce. pip install brasscoders, point it at any Python project with a pickle.load, pickle.loads, or bare yaml.load call, and the B301 or B506 finding shows up in the scan output with file, line, and severity. Run it against a project that loads Hugging Face checkpoints and check whether the loading code pins a framework version alongside the safety flag — the version is the part a quick read-through tends to skip.

The full research index entry for this category has the complete CVE list, the primary-source advisories, and the OWASP remediation reference, kept current as new advisories land. The OSS core is free and Apache 2.0 licensed; BrassCoders Paid adds AI-powered enrichment on top at $12/dev/month.

Frequently Asked Questions

Has pickle.load ever caused a real CVE?

Yes, though the vulnerability usually gets attributed to whatever ships the deserialization call rather than to pickle.load itself. CVE-2017-18342 hit PyYAML's yaml.load at CVSS 9.8, and more recent CVEs like CVE-2025-32434 in PyTorch's torch.load and CVE-2026-12484 in Keras's TorchModuleWrapper show the same bug class reappearing inside ML-framework model loading a decade later.

Is torch.load(weights_only=True) actually safe?

Not on its own. CVE-2025-32434, CVSS 9.3, showed that PyTorch's documented mitigation, setting weights_only=True, still allowed remote code execution on PyTorch 2.5.1 and earlier. The fix was upgrading PyTorch to 2.6.0, not adding the flag. Confirm both the flag and the version before trusting a checkpoint load.

Does BrassCoders detect unsafe deserialization?

BrassCoders' Bandit integration flags pickle.load, pickle.loads, and cPickle calls under rule B301, and unsafe yaml.load calls under rule B506, by pattern on every scan. BrassCoders does not determine whether the specific input is actually untrusted; that context call belongs to the AI triage layer reading the surrounding code.

Are malicious ML models actually being distributed, or is this theoretical?

It's active, not theoretical. JFrog's security research team found close to 100 malicious models on Hugging Face carrying real payloads in February 2024, PyTorch pickle files the most common carrier, including one model that opened a reverse shell to an attacker's server the moment it loaded.

What should I use instead of pickle or yaml.load for untrusted data?

The OWASP Deserialization Cheat Sheet gives two general fixes: prefer a plain data format like JSON that can't encode executable objects, or cryptographically sign serialized data and reject anything unsigned before deserializing it. For YAML specifically, yaml.safe_load is a one-word swap that removes the code-execution path entirely.

Why does this bug keep reappearing in newer ML frameworks instead of getting fixed once?

Because each framework re-implements the same convenience: load an object graph directly from a file with one function call. CVE-2017-18342 was YAML's default Loader; CVE-2025-32434 was PyTorch's weights_only flag; CVE-2026-12484 was Keras's TorchModuleWrapper. Three different codebases, one repeated design choice: trust the file, then reconstruct whatever object graph it describes.