Programs often need to save an object or send it to another program: a shopping cart, a session, a job for a queue, a machine-learning model. To do that they flatten it into bytes, and later they rebuild it. The first step is serialization; the second, , is where this lesson's bug lives.
Flattening and rebuilding
Think of flat-pack furniture. is taking the finished cupboard apart into a box with a parts list. Deserialization is following the list to rebuild it. For ordinary data this is safe: a list of numbers goes into the box and comes back out.
The trouble starts with formats whose "parts list" can say more than "put this part here". Some formats can describe which functions to call while rebuilding, because they are meant to restore rich objects with their behaviour. Python's pickle and Java's native object serialization are well-known examples. In those formats, the bytes are not just data. They are closer to a small program the loader obeys.

That is the flaw. If a program loads bytes that an outsider supplied, using a format that can carry instructions, the outsider writes part of the program that runs. It is another injection flaw: data and instructions share a channel, and the receiver trusts the channel.
A harmless demonstration
Python's own documentation warns never to unpickle data from an untrusted source, and you can see the reason without anything dangerous. This class tells the pickle format, when rebuilt, to call print:
import pickle
class Demo:
def __reduce__(self):
return (print, ("this ran while the data was being LOADED",))
blob = pickle.dumps(Demo())
pickle.loads(blob)
Creating the bytes with dumps prints nothing, but loading them with loads prints the sentence. The loader called a function named inside the data. We chose print. A stranger who controls the bytes chooses the function, and a real attack names something far more capable than print. The point: loading is executing.
Why it is hard to spot
Deserialization bugs hide in plain sight because the vulnerable line looks like routine plumbing: loads, readObject, unserialize, Marshal.load. Reviewers read it as "restore the saved object", not "run input". The bytes can also arrive from places that feel internal: a cookie, a message queue, a cache entry, a file in shared storage, a downloaded model or dataset, or another service's API reply. If anyone untrusted can influence it, it is an input, and the checklist from the first lesson applies.
Java made this famous at scale, partly because large applications include many libraries on the classpath, each with classes that can be chained together by a crafted byte stream. That detail explains why a bug in a deserializer can be exploitable even when your own code never intended anything dangerous: the parts lying around in your dependencies are the raw material.
The fix
Ranked from strongest:
1. Do not deserialize untrusted data with a code-capable format. Use a data-only format such as JSON, which has no way to name a function to call, and rebuild your objects yourself from the plain values.
import json
def load_cart(text):
data = json.loads(text)
items = data["items"]
if not isinstance(items, list) or len(items) > 100:
raise ValueError("bad cart")
return [(str(i["sku"]), int(i["qty"])) for i in items]
Parsing gives plain data. Your code then validates it against what it expects, the allowlist habit from lesson one, and constructs the real objects.

2. If you must use a rich format, restrict it. Some platforms support allowlisting which classes may be rebuilt (for example, Java deserialization filters). An allowlist of a few known-safe types is far better than the default of "anything available".
3. Prove where the bytes came from. Sign data you hand out (an HMAC with a secret key) and verify the signature before loading. This works only if the key is secret and the check happens first. It stops outsiders forging bytes, though not an insider who holds the key.
4. Reduce the damage. Run the loading code with least privilege, in a sandbox, with no secrets in reach. Keep libraries updated, since many fixes in this area are patches that remove or filter dangerous classes.
Why does switching from pickle to JSON remove the code-execution risk, even before you validate?
JSON can only describe plain values such as text, numbers, lists and maps. It has no way to name a function for the loader to call, so parsing it cannot run code. You still validate it for correctness, but the dangerous capability is gone.
Spotting it in a code review
- Search for the load calls of code-capable formats:
pickle.load,yaml.load(without the safe loader),ObjectInputStream,readObject,unserialize,Marshal.load,BinaryFormatter. - Follow the argument to its source. Cookies, headers, request bodies, queues, caches, uploaded files and fetched files are all untrusted unless proven otherwise.
- Look for a signature check before loading, not after.
- Check whether the library versions are current, since many incidents come from old deserialization gadgets.
A real case
Two lessons sit in that note. One is the root cause, CWE-502, deserialization of untrusted data in a widely deployed help-desk product, which can yield command execution on the server. The second is how to handle uncertainty: when a vendor cannot tell exactly who can reach a bug, patching is the safe move. The record scores it 9.8 critical and CISA added it to its Known Exploited Vulnerabilities list within days of publication, which tells you how quickly deserialization bugs in internet-facing products draw attention.
How AI changes this
Attackers. Deserialization exploitation used to demand deep knowledge of one language's libraries: finding chains of classes that, when rebuilt in a certain order, end in something dangerous. That is exactly the kind of tedious code-reading search that models speed up. Beam's blog describes a reported July 2026 incident in which an autonomous AI system chained a deserialization leak with another flaw, according to reports that are not fully confirmed. Treat such accounts as signals of direction, not settled fact.
Defenders. Assistants can list every load call of a code-capable format in a repository and propose a JSON-and-validation replacement, which is a mechanical, reviewable change. They are also useful for explaining library advisories. Verify any migration with tests, because swapping formats can change behaviour.
AI systems themselves. AI is the new home of this bug, because machine-learning artefacts are routinely shipped as serialized objects. Pickle-based model files, agent "memory" stores and cached tool outputs may be reloaded across sessions, which makes them an input with code-execution potential. The same blog report mentions earlier, separate pickle-based attacks on a public model hub. If an agent stores state and reloads it later, that state is a deserialization input, and poisoned memory persists across runs. Prefer data-only formats for agent state, load models from sources you trust and verify, and keep the loading process unprivileged.
Try it
You will see the difference between code-capable and data-only loading on your own machine. Nothing in this lab is dangerous: the "payload" is a print statement you write.
Save as deser_lab.py:
import json, pickle
class Demo:
def __reduce__(self):
return (print, ("PICKLE: code ran during loading",))
evil_looking = pickle.dumps(Demo())
print("loading pickle bytes...")
pickle.loads(evil_looking)
text = '{"items": [{"sku": "abc", "qty": 2}]}'
data = json.loads(text)
print("JSON loaded, plain data only:", data)
try:
json.loads(evil_looking.decode("latin-1"))
except ValueError:
print("JSON refused the pickle bytes: not data it understands")
Run python3 deser_lab.py. The pickle line prints a sentence from inside the loader; the JSON parser only ever yields plain data and refuses what it does not understand. Then read the Python documentation's warning box for pickle and note the sentence that says to only unpickle data you trust. Delete the file when done.
Check yourself
Why is 'we only load data from our own cookie' not automatically safe?
A cookie lives in the visitor's browser, so the visitor controls it. Unless it is signed and the signature is verified before loading, the bytes are untrusted input, however internal the code path feels.
You must keep a rich format. Name two controls.
Allowlist the classes that may be rebuilt (a deserialization filter), sign the data and verify the signature before loading, run the loader unprivileged in a sandbox, and keep libraries patched.
What did the note in the SolarWinds record teach about handling uncertain bugs?
Even when the vendor could not reproduce the flaw without authentication, it advised applying the patch. When you cannot be sure who can reach a critical bug, patching beats arguing about reachability.
Next
Deserialization worked through bytes in transit. The next lesson looks at one more kind of input we have only brushed past: files that visitors upload, and what happens when a profile photo turns out to be a program.

