Avoid pickle for untrusted data

Avoid pickle for untrusted data — Secure development.

Focus: avoid pickle for untrusted data

Sponsored

Imagine an attacker slips a malicious file onto your server — a cache file, an upload, a message-queue payload. Your code calls pickle.load(), and within milliseconds, the attacker has executed arbitrary code on your machine. This isn't a rare theoretical threat; it's one of the most practical and devastating deserialization vulnerabilities in Python. This lesson teaches you why pickle is dangerous for untrusted data and how to replace it with safe alternatives.

The problem this lesson solves

Python's pickle module is incredibly convenient: one line to serialize an object, one line to restore it. But that convenience comes with a hidden cost — pickle is not safe for untrusted data. If an attacker can supply even one byte of a pickle stream, they can craft it to execute arbitrary code when you call unpickle.load().

Consider a typical scenario: a web application caches user session data to disk, or a microservice reads a job definition from a queue. The developer thinks, "I'll just pickle this — it's quick." Later, an attacker uploads a specially crafted pickle instead of a normal session. When the server unpickles it, the attacker's commands run with the server's privileges. The result: data theft, ransomware, or a full system takeover.

The stakes are high because pickle is often used in internal tools, data pipelines, and machine-learning model distributions — places where developers assume trust but shouldn't. This lesson gives you a clear rule: never call pickle.load() or pickle.loads() on bytes you haven't fully trusted and who's origin you can verify end-to-end. Then it shows you what to use instead.

Core concept / mental model

Think of pickle as a ticking time bomb in a gift box. The box looks harmless — a serialized object — but the bomb (the __reduce__ method) can be armed by anyone who can write to the file. When you open the box (load()), the bomb explodes (arbitrary code execution).

Here's what happens under the hood: pickle is not a data format like JSON — it's a virtual machine with opcodes. When you unpickle, Python reconstructs objects by executing a sequence of these opcodes. Some opcodes, like REDUCE, call a callable with arguments. If an attacker controls the stream, they can make that callable os.system and the argument "rm -rf /".

In contrast, safe formats like JSON or MessagePack have no code execution — they only represent data structures (dicts, lists, strings, numbers). So the mental model is:

  • Untrusted data = any bytes that came across a network, from a user, or from a file you didn't personally write.
  • Pickle = dangerous because it can invoke code.
  • Safe alternatives = data-only serialization.

This is why the Python documentation literally warns: "The pickle module is not intended to be secure against erroneous or maliciously constructed data."

How it works step by step

Let's break down why pickle can execute code, step by step:

  1. Serialization: When you call pickle.dump(obj, file), Python writes a stream of opcodes that describe how to reconstruct the object.
  2. Deserialization: When you call pickle.load(file), Python reads those opcodes and executes them in a restricted interpreter — but it's not restricted enough.
  3. The attack vector: The __reduce__ method (or __reduce_ex__) lets a class define how it should be unpickled. This method can return a tuple like (callable, (args,)). When pickle encounters it, it calls callable(*args).
  4. Exploitation: An attacker crafts a pickle stream that sets __reduce__ to return (os.system, ("touch /tmp/pwned",)). When you call load(), the system command runs.

Here's a minimal proof of concept:

import pickle
import os

class Evil:
    def __reduce__(self):
        return (os.system, ("echo PWNED > /tmp/pwned.txt",))

# Attacker crafts this payload
malicious = pickle.dumps(Evil())

# Victim's code
with open("data.pkl", "wb") as f:
    f.write(malicious)

# Later, victim loads it
with open("data.pkl", "rb") as f:
    pickle.load(f)  # Executes the echo command

print(open("/tmp/pwned.txt").read())  # PWNED

This is not a bug in pickle; it's by design. pickle trusts the serialized data completely, and that trust is the vulnerability.

Hands-on walkthrough

Let's practice a safe, hands-on example. You'll see the attack in action, then compare it to safe alternatives.

Step 1: Demonstrate the vulnerability (in a sandbox)

First, create a malicious pickle payload and verify it executes a command. Never run this on a production system.

# generate_malicious.py
import pickle
import os

class Evil:
    def __reduce__(self):
        return (os.system, ("echo VULNERABLE > /tmp/evil.txt",))

payload = pickle.dumps(Evil())
with open("evil.pkl", "wb") as f:
    f.write(payload)

print("Payload written")

Now run it and then load it:

python generate_malicious.py
python -c "import pickle; pickle.load(open('evil.pkl','rb'))"
cat /tmp/evil.txt  # Outputs VULNERABLE

Step 2: Use a safe alternative — JSON

import json

data = {"user": "alice", "role": "admin"}

# Serialize
json_str = json.dumps(data)

# Deserialize — no risk of code execution
loaded = json.loads(json_str)
print(loaded)  # {'user': 'alice', 'role': 'admin'}

Step 3: Use ast.literal_eval for simple data

If you absolutely must read a Python literal from a file, use ast.literal_eval — it only evaluates literals, not arbitrary code.

import ast

file_content = "{'user': 'bob', 'access': 3}"
data = ast.literal_eval(file_content)
print(data)  # {'user': 'bob', 'access': 3}

Expected output for all examples is shown in comments — you can run these in a sandbox to see them.

Compare options / when to choose what

Format Safe for untrusted data? Supports custom objects? Use case
pickle No Yes, via __reduce__ Never for untrusted data
JSON Yes No, only built-in types API responses, config files, most web data
MessagePack Yes (with trusted validator) No High-performance network protocols
YAML (with safe loader) Yes (with yaml.safe_load) No Configuration files, human-readable data
ast.literal_eval Yes (limited) No Parsing Python literal syntax from trusted-ish sources

Variations to consider: - For machine learning models, use torch.load(..., weights_only=True) in PyTorch 2.6+ or the safetensors library instead of raw pickle. - For performance-critical binary data, use pickle only with data you fully control (e.g., internal cache that never touches the network). - For cross-language compatibility, use JSON or MessagePack — they avoid Python-specific quirks.

When you must accept pickle for legacy reasons, validate the stream with a cryptographic signature, but even that is risky — better to migrate away.

Troubleshooting & edge cases

Issue 1: You think your data is safe because it's from an internal service.

Even internal data can be compromised — a compromised microservice becomes an untrusted source. Always assume data crossing any boundary is untrusted.

Issue 2: JSON doesn't support tuples or custom classes. Fix: Convert to dictionaries before serializing, or use ast.literal_eval for simple structures.

Issue 3: You see an error AttributeError: Can't get attribute 'ClassName' when unpickling. This happens when the class is missing or renamed. That's a sign you're coupling data to Python implementation — switch to JSON to avoid it.

Issue 4: Performance concerns with JSON. If speed matters, use pickle only for trusted data, or use MessagePack. But never sacrifice security for speed without a clear threat model.

Issue 5: yaml.load is dangerous by default. Always use yaml.safe_loadyaml.load can execute arbitrary Python objects.

Issue 6: Legacy pickle caches from before you knew better. Write a migration script to convert them to JSON and then delete the pickle files. Do it as soon as possible.

What you learned & what's next

You've learned why avoid pickle for untrusted data is a critical security rule: pickle executes arbitrary code when loading, opening the door to remote code execution. You can now:

  • Explain the core idea behind the rule and the __reduce__ exploit.
  • Complete a practical exercise using JSON or ast.literal_eval to safely deserialize data.
  • Understand when to choose JSON, MessagePack, or safe YAML over pickle.
  • Troubleshoot common errors and migration paths.

Next in this Secure development track, you'll explore input validation posture — how to sanitize and validate all data entering your system, so even if an attacker sends malicious payloads, they can't cause harm. You've taken a key first step in building secure Python applications: never trust the data you didn't write.

Keep this rule on your security checklist: if you're about to call pickle.load, stop and think about where those bytes came from.

Practice recap

Create two small scripts: one that generates a malicious pickle payload, and another that safely loads a JSON equivalent. Run the malicious one in a sandbox to see the command execute, then confirm JSON load does nothing dangerous. This hands-on gap reinforces the rule.

Common mistakes

  • Using pickle.load() on data from a network socket or user upload because it's 'internal' — always assume it's untrusted.
  • Thinking JSON is unsafe because it can't handle tuples — convert to lists or use ast.literal_eval for simple cases.
  • Using yaml.load() instead of yaml.safe_load() — the former can execute arbitrary code, just like pickle.
  • Migrating to pickle for speed in a web service without re-evaluating the threat model — security first.
  • Forgetting to convert legacy pickle caches to safe formats before they're exploited.

Variations

  1. Use JSON for all data interchange; it's safe and language-agnostic.
  2. Use ast.literal_eval when you need Python-literal syntax from trusted sources.
  3. For ML models, use safetensors or torch.load(..., weights_only=True) instead of raw pickle.

Real-world use cases

  • A web app accepts user-uploaded config files — deserialize with JSON to prevent RCE.
  • A microservice bus uses MessagePack instead of pickle for job payloads.
  • A data pipeline replaces pickle caches with JSON files to survive container restarts safely.

Key takeaways

  • Pickle can execute arbitrary code when loading, so never use it on untrusted data.
  • Safe alternatives include JSON, MessagePack, and safe YAML — none of them execute code.
  • If you must load legacy pickle, validate the stream cryptographically, but prefer migration.
  • Assume any data crossing a network boundary is untrusted.
  • Use ast.literal_eval for parsing Python literals from safe sources.
  • Always use yaml.safe_load over yaml.load.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.