pickle is Python's own native object serialization format, and it's genuinely useful for exactly what it's designed for: serializing arbitrary Python objects between trusted parts of your own system. It is also one of the most consistently documented sources of remote code execution vulnerabilities in real Python software, whenever that boundary is crossed.
1Why pickle Is Powerful: Full Object Graph Serialization
pickle is Python's own, native serialization format, and its core capability genuinely is powerful and useful: unlike JSON, which can only represent plain values, pickle.dumps() can serialize essentially *any* Python object ā a class instance with its full attribute state, nested custom objects, even certain function and class references ā and pickle.loads() reconstructs that exact object graph, fully rehydrated, on the other end. This makes pickle genuinely valuable for legitimate, specific use cases: caching a complex computed Python object to disk between runs of your own script, or passing arbitrary Python objects between processes via multiprocessing (which uses pickle internally, as covered in the Multiprocessing lesson).
This capability is a direct, severe instance of the 'object-construction format' category from the previous lesson's general framework ā pickle's entire design purpose *is* reconstructing arbitrary objects from serialized data, which is precisely the capability that lesson identified as inherently risky for untrusted input. Pickle isn't a plain-data format that happens to have a dangerous edge case; object reconstruction is its fundamental, core, by-design behavior.
Understanding pickle's genuine, legitimate power is important precisely because it explains why the format exists and continues to be used at all, despite the severe risk covered next ā the risk isn't a design flaw or an oversight; it's the direct, unavoidable consequence of the exact capability that makes pickle useful for its actual, legitimate purposes.
import pickle
class User:
def __init__(self, name):
self.name = name
user = User("Ada")
data = pickle.dumps(user) # serializes the FULL object, not just plain data
restored = pickle.loads(data) # reconstructs an actual User instance
print(restored.name) # 'Ada'Full object reconstruction ā genuinely useful for trusted, internal data
2__reduce__: The Exact Mechanism Behind the Vulnerability
__reduce__, a method any class can define, is pickle's own, entirely legitimate mechanism for controlling exactly *how* an instance of that class gets reconstructed during unpickling ā it returns a tuple of (callable, arguments), and pickle's actual reconstruction process is, precisely, calling that callable with those arguments. This is genuinely useful for classes with complex internal state that can't be reconstructed simply by restoring attribute values directly (a class wrapping a file handle or a network connection, for instance, might need __reduce__ to specify how to properly re-establish that resource on the receiving end).
The vulnerability is a direct, unavoidable consequence of this exact, legitimate mechanism: __reduce__ can return *any* callable ā there's no restriction requiring it to be 'safe' or limited to the defining class's own methods. return (os.system, ("echo 'this could be ANY command'",)) is entirely valid, well-formed pickle protocol usage from pickle's own perspective; pickle.loads() faithfully does exactly what the serialized data instructs it to do, which is calling os.system with the attacker's chosen argument ā genuine, unrestricted code execution on whatever machine calls pickle.loads() on that data.
This is why the vulnerability isn't a bug that a future pickle update could patch ā it's the direct, necessary consequence of __reduce__'s legitimate design purpose. Fixing it would require removing the exact capability (arbitrary callable invocation during reconstruction) that makes pickle's legitimate use cases possible in the first place, which is precisely why no 'safe mode' for pickle exists or can meaningfully exist.
import pickle, os
class MaliciousPayload:
def __reduce__(self):
# __reduce__ tells pickle HOW to reconstruct this object --
# here, it tells pickle to call os.system('rm -rf ~') INSTEAD
return (os.system, ("echo 'this could be ANY command'",))
payload = pickle.dumps(MaliciousPayload())
# pickle.loads(payload) -- would ACTUALLY RUN that command. This is not theoretical.Legitimate mechanism, weaponized ā pickle faithfully executes it either way
3The Unconditional Rule, and What to Use Instead
Given that pickle's core capability *is* arbitrary callable invocation during reconstruction, and no restricted-but-still-functional 'safe mode' exists (unlike YAML's safe_load), the rule is genuinely unconditional and without practical exception: never call `pickle.loads()` (or `pickle.load()`) on data from any source you don't fully, permanently, and completely trust ā no user upload, no network request body, no external API response, no data received from a source outside your own directly-controlled system, ever.
Pickle remains entirely appropriate for its genuine, legitimate use cases: data your own code both writes *and* reads, never crossing any external trust boundary at all ā caching a complex Python object between runs of a script only you control, or multiprocessing's internal use of it to pass objects between worker processes you yourself spawned. The risk materializes specifically and only when unpickling crosses a genuine trust boundary; pickle used entirely within a system you fully control carries no more inherent risk than any other internal data structure.
For any scenario needing to serialize data that *might* cross a trust boundary ā even one you currently believe is fully trusted, since trust boundaries have a way of shifting as systems evolve ā default to a plain-data format (JSON, or a purpose-built binary format like msgpack or Protocol Buffers, both of which are limited to plain-data representation, not arbitrary object reconstruction) instead. The engineering discipline this establishes: treat pickle as a tool for a genuinely narrow, specific use case (trusted, internal-only round-tripping), not a general-purpose serialization default, ever.
# NEVER do this on external, untrusted, or user-provided data:
# pickle.loads(request.data)
# pickle.loads(uploaded_file.read())
# Pickle is appropriate ONLY for data your OWN code both wrote AND
# will read, never crossing any external trust boundary at allNever on anything crossing a trust boundary ā no exceptions
4Step-by-Step Breakdown
pickle.loads(untrusted_bytes) can execute arbitrary code on your machine ā not hypothetically, but as pickle's actual, documented, by-design behavior. This is the single most important security fact to know about the Python standard library.
pickle.dumps()/loads() can serialize and reconstruct ARBITRARY Python objects -- classes, instances, nearly anything -- which is exactly its power AND its danger.
Because pickle can reconstruct ARBITRARY objects, it can be instructed to call ANY callable during deserialization -- including os.system.
Checkpoint: What does __reduce__ let a malicious pickled payload do during unpickling?
- āSpecify ANY callable (like os.system) and its arguments to be invoked as part of reconstructing the object -- genuine, arbitrary code execution
- āOnly specify which attribute VALUES the reconstructed object should have
The rule, without exception: NEVER call pickle.loads() on data from any source you don't fully and permanently trust -- there is no 'safe_load' equivalent for pickle.
Checkpoint: Is there a "safe_load"-equivalent restricted mode for pickle, the way yaml.safe_load() exists for YAML?
- āNo ā pickle's core design IS arbitrary object reconstruction; there is no restricted mode that preserves its functionality while eliminating this risk
- āYes ā pickle.safe_loads() provides the same protection yaml.safe_load() does
Pickle demonstrates the object-construction-format risk from the previous lesson in its most concrete, severe form; Dependency Security closes this section by looking at risk from a different angle entirely ā the packages you depend on, not the data you parse.
Round-Trip a Real Pickled Object. Finish round_trip_user(): pickle reconstructs arbitrary Python objects, not just plain data.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Best Practices
Never call pickle.loads()/pickle.load() on data from any external, untrusted, or user-provided source, without exception
There is no restricted 'safe mode' for pickle the way yaml.safe_load() exists for YAML ā the risk is inherent to pickle's core design, not a configurable option that can be disabled while keeping the format functional.
Reserve pickle exclusively for data your own code both writes and reads, entirely within a trusted system boundary
This is pickle's genuine, legitimate use case ā for anything that might ever cross an external trust boundary, use a plain-data format (JSON, msgpack, Protocol Buffers) instead.
Frequent Bugs
Using pickle to serialize data that will be received from an external source (a user upload, a network request, a message queue consumed from outside the immediate system), creating a remote code execution vulnerability the moment untrusted data reaches pickle.loads().
Replace pickle with a plain-data format (JSON, msgpack) for any serialization scenario where the data could ever originate from, or be tampered with by, a source outside your own directly-controlled system.
Real-World Examples
Replacing an Unsafe Pickle-Based Cache With JSON
A caching layer was originally built using pickle to store complex Python objects, but a security review discovers cache entries could theoretically be influenced by external input, requiring a switch to a safe alternative.
# Before: unsafe if cache entries could ever be influenced externally
import pickle
cached_data = pickle.loads(redis_client.get(cache_key))
# After: safe regardless of how the cache is populated
import json
cached_data = json.loads(redis_client.get(cache_key))
# Requires the cached objects to be representable as plain JSON data --
# a worthwhile constraint given the alternative's severity