Insecure Deserialization
Turning Data Back Into Code
Serialization converts an in-memory object into a format that can be stored or transmitted—bytes, JSON, XML, or a language-specific binary blob. Deserialization reverses it. When an application deserializes data from an untrusted source, an attacker can often control not just the values but the type and behavior of the object being reconstructed. In the worst case, that means remote code execution.
Why It Is So Dangerous
The vulnerability arises because many serialization formats encode the class of the object, not just its data. If the deserializer is allowed to instantiate arbitrary classes, and any of those classes have side effects in their constructors, destructors, or special methods, the attacker can chain those side effects into a gadget chain that executes arbitrary code—without ever injecting a line of code themselves.
Python: pickle
Python's pickle format can execute code during deserialization via the __reduce__ method. A crafted payload can call any importable function:
import pickle, os
class Exploit:
def __reduce__(self):
return (os.system, ("id",))
payload = pickle.dumps(Exploit())
# If the server does pickle.loads(request.data), this runs `id`.
This is why the Python documentation explicitly warns: never unpickle data received from an untrusted source. Libraries like joblib, dill, and PyYAML's unsafe yaml.load carry the same class of risk.
Java, PHP, and .NET
- Java:
ObjectInputStream.readObject()on untrusted input is the classic sink. Rich gadget libraries (Apache Commons Collections, Spring) have been chained into some of the most devastating RCEs in history. - PHP:
unserialize()combined with magic methods (__wakeup,__destruct) and POP chains leads to object injection, often escalated to file writes or RCE. - .NET:
BinaryFormatterandLosFormatterare notoriously unsafe and have been deprecated for this reason.
Object Injection Without RCE
Even without a full gadget chain, tampering with serialized objects lets attackers modify fields they should not control: change a user's role from user to admin, alter prices, or bypass integrity checks. This is object injection, and it can be a critical authorization bypass on its own.
Defenses
- Do not deserialize untrusted data with native formats. Use a data-only format—JSON, Protocol Buffers, or MessagePack—that cannot express behavior.
json.loadscannot execute code. - Never use
pickle/BinaryFormatter/ObjectInputStreamfor untrusted input. If you must exchange signed blobs, sign them with HMAC and verify before deserializing. - Allowlist types. Where a framework requires deserialization, restrict it to an explicit set of safe classes.
- Keep libraries patched. Many gadget chains depend on known-vulnerable versions.
- Isolate and least-privilege. A service that must deserialize risky data should run with minimal permissions so a compromise is contained.
The lesson is blunt: deserialization is a code-loading mechanism wearing a data-parsing costume. Treat it with the caution that implies.
A Realistic Attack Chain
The danger becomes concrete when you trace a chain. A Java service accepts a base64-encoded serialized session object in a cookie. The attacker decodes the cookie, swaps in an object from a library on the classpath that has a dangerous readObject method, and re-encodes it. When the server deserializes the cookie, the library runs attacker-controlled logic that eventually invokes Runtime.exec, granting remote code execution—without the attacker ever sending a shell command. The same shape recurs in PHP (a __destruct method that writes a file) and Python (a __reduce__ that calls os.system). Defending in depth means removing the sink entirely: no native deserialization of untrusted input, integrity checks where exchange is unavoidable, patched dependencies to break known gadgets, and least-privilege runtimes so a successful chain has little to reach.