Insecure Deserialization in Java and Python Applications
Attackers can execute arbitrary code by exploiting how Java and Python deserialize untrusted data.

Serialization is the process that takes an object sitting in memory and turns it into something that can be stored on disk or sent over a wire: binary data, or structured text like JSON, XML, or YAML. Deserialization does the reverse, taking that stored or transmitted format and rebuilding the original object. Neither half of this is exotic. It's baked into how applications persist data, cache results, talk to other processes, hand out API session tokens, and set cookies.
Almost every major runtime ships this capability out of the box. Java has ObjectInputStream, Python has the pickle module, PHP has serialize() and unserialize(), and.NET had BinaryFormatter as its native binary serializer until it was pulled starting in.NET 9. The mechanism matters so much from a security standpoint because it's everywhere, running quietly in the background of systems most engineers never think twice about.
The liability appears the moment a deserializer accepts input from somewhere it shouldn't trust. And that input rarely announces itself. Serialized data slips into cookies, HTTP parameters, binary protocols, message queue payloads, and API responses, channels that engineers building and reviewing application logic rarely scrutinize for object injection. Nobody's staring at a cookie value wondering if it's secretly a fully-formed Java object waiting to reconstruct itself into something dangerous. That blind spot is the root condition for everything that follows.
The severity range: from denial of service to unauthenticated remote code execution
At the top of the severity range sits remote code execution, and that's the number that should get anyone's attention. A successful deserialization exploit can hand an attacker a shell on the application server itself, meaning code execution on the machine, not just access to whatever data the app happens to expose. That represents the ceiling on how bad the impact can get. Below it, the range still does real damage: authentication bypass, where an attacker forges a session object or a ViewState object and impersonates another user without ever touching valid credentials. Below that, denial of service, where malformed input crashes or hangs the deserializer.
None of these outcomes require a second vulnerability to be present first. The deserialization flaw is sufficient on its own.
The relief valve is a modest one. Prevalence looks low: Veracode's own documentation puts the share of applications affected at a small fraction. Contrast Security's 2021 Application Security Observability Report is roughly the same place, describing a similarly small proportion of applications carrying serious insecure deserialization vulnerabilities. But that same report deflates any comfort almost immediately: the applications that do carry this flaw can see an extremely high volume of attacks against them every month. Low prevalence still draws heavy attention from attackers. It means the ones who find it, hammer it. OWASP ranked insecure deserialization #8 on its Top Ten list of most critical web application security risks since 2017, and the 2021 update folded the standalone A8:2017 category into A08:2021-Software and Data Integrity Failures.
How Java deserialization exploitation works: gadget chains from classpath to shell
Java is where this vulnerability class gets its most mature, most heavily documented form. The method at the center of it all is ObjectInputStream.readObject(), which takes a byte stream and reconstructs it into a live object. Serialized Java objects carry a signature: the hex bytes AC ED 00 05, or rO0AB when base64-encoded. Any pen tester worth their fee checks for those bytes first, at every input channel, before doing anything else.
The danger kicks in once readObject() runs on data an attacker controls. At that point, the attacker chooses which classes get instantiated and what methods run during the reconstruction process. That's the mechanism behind what's called a gadget chain: a sequence of classes, already sitting on the application's classpath, wired together so that the act of deserializing triggers one method call after another, cascading through what are often called magic methods, until it lands on a dangerous sink like Runtime.exec(). The attacker doesn't smuggle in new code. They repurpose code that's already there, legitimately, doing something else entirely under normal conditions.
The scale of this problem comes down to how much raw material a typical Java classpath offers. Any library doing reflection, dispatching methods based on attacker-controlled fields, or implementing custom deserialization logic is a candidate gadget source, and enterprise Java applications tend to carry dozens of such libraries. Apache Commons Collections became the poster child here, a library so widely deployed and so rich in exploitable gadget material that it's now the textbook example. But the problem doesn't stop at the JDK's native format. Moritz Bechler's marshalsec research mapped gadget potential across Jackson, XStream, Kryo, Hessian, and JYaml, making clear that "Java deserialization" is a much bigger territory than ObjectInputStream alone.
The attack flow itself follows a predictable arc. An attacker finds where serialized data enters the application, a cookie, an HTTP parameter, a message queue message. From there, they pick a gadget chain that matches whatever libraries sit on the target's classpath, generally using a tool like ysoserial to generate the payload, encode it correctly for the injection point, and send it in. The server deserializes the object, the chain walks through its sequence of method calls, and the final gadget fires, usually landing a reverse shell or dropping a web shell to disk. Code execution on the server, achieved through data the application thought it was just reading back into memory.
The unsettling part is how much of this space current tools still miss. Research out of Yangzhou University, Ant Group, Tsinghua, and East China Normal University, published as ODDFUZZ, tested itself against the known chains catalogued by ysoserial and found 16 out of 34. Two other state-of-the-art baseline tools, tested the same way, found three. ODDFUZZ went further, surfacing 6 previously unreported exploitable gadget chains, which led to 5 new CVEs across real production software including Oracle WebLogic Server, Apache Dubbo, Sonatype Nexus, and protostuff. Sixteen out of thirty-four is less than half. That's the honest state of detection tooling against a known, cataloged threat, let alone the gadgets nobody's found yet.
The dormant gadget chain problem: how supply chain changes silently create new attack paths
Everything above assumes the gadget chains in question already exist somewhere in a dependency tree, waiting to be found. Research from Umeå University, led by Kreyssig, Riom, Houy, and Bartel, asked a more unsettling question: how does a library's exposure to this problem change over time, as the library itself evolves? The team studied 1,475 widely used Maven dependencies across 111,275 versions, tracking how class serializability shifts as a project moves forward through its release history, a longitudinal record across a moving target. That's a longitudinal record across a moving target. That's a longitudinal look at a moving target.
From there, the researchers applied three modification patterns, changes that an attacker could introduce quietly, or that a developer could introduce completely by accident, to 533 dependencies that already contained at least one serializable class. A large share of the confirmed cases needed only one of those three patterns to open up new exposure, a strikingly low bar. That's a strikingly low bar. One small, plausible-looking change, and a dependency that was previously clean can start carrying gadget material.
One pattern deserves particular attention because it's so easy to miss in a normal code review. Marking an interface as Serializable transitively makes every subtype of that interface serializable too, even ones a maintainer never touched and never intended to expose. A single line, buried in an interface definition, and the whole inheritance tree beneath it inherits the exposure. Nobody reviewing that diff is likely to trace every downstream class that implements the interface.
ysoserial, the reference tool for Java gadget chains, last added a chain in February 2021 and is losing relevance for current software; since 2020, the Umeå University paper found more than 30 new critical vulnerabilities have been published in the NVD under CWE-502 with a CVSS v3 score of 9 or above. The tooling stood still. The vulnerability class kept moving. A dependency update that sails through every scanner today can quietly activate a dormant gadget chain tomorrow, and detection, as it stands, is reactive rather than predictive.
Attackers, for their part, aren't waiting around either. Synacktiv published research showing how to make Java deserialization exploits harder to catch once a working RCE chain has already been identified, specifically by recompiling the gadget-chain-generating projects with renamed modules, packages, and class names to slip past signature-based WAF detection. Defenders are chasing dormant chains they don't know exist yet, while the offensive side is actively working on ways to make the known chains invisible to the tools built to catch them.
Python deserialization: how pickle exploitation works in ML pipelines
Python's version of this problem looks nothing like Java's, and it's arguably more direct. The pickle module executes arbitrary code during deserialization as a designed feature, not a bug that slipped through. Any endpoint that calls pickle.loads() on data supplied by a user is exploitable, full stop.
The mechanism runs through a method called reduce. A class can define reduce to control what gets called when it's unpickled, and an attacker simply writes a reduce that returns a callable plus its arguments. Pickle executes that callable unconditionally the moment it loads. There's no gadget chain to assemble here. Less engineering, less searching, faster time to impact.
Fingerprinting pickle data follows the same logic as fingerprinting Java's serialized format. Pickle streams showing up in cookies or API parameters typically appear as base64-encoded strings starting with gASV for protocol 4, or KGxp for older protocols. A tester grep's for those prefixes across captured HTTP traffic and stored session data.
The part of this story that makes it feel newly urgent, rather than a rehash of an old problem, is machine learning. Model files distributed for reuse are, in a huge number of cases, pickle-backed under the hood. A 2025 Brown University study found that roughly half of popular HuggingFace repositories still contain pickle-backed model files, and that list includes releases from Meta, Google, Microsoft, NVIDIA, and Intel. These are models with corporate backing behind them, distributed by major engineering organizations. These are models coming from some of the largest engineering organizations on the planet, distributed through a format that executes code on load.
It gets worse before it gets better. In February 2025, researchers managed to bypass every major pickle scanner in circulation and achieve remote code execution against models hosted on Hugging Face. Any team pulling down a model file and loading it with torch.load() or an equivalent, without sandboxing that load step, is running arbitrary code written by whoever authored the file, or by anyone who touched it in transit. That's a live supply chain risk. That's the current state of a widely used distribution channel.
PyYAML rounds out the Python picture as a quieter, secondary vector. CVE-2026-24009, the Docling RCE, traces back to exactly this pattern: a shadow vulnerability introduced through use of PyYAML's unsafe FullLoader, exploitable wherever the vulnerable PyYAML configuration is present. Old defaults have a way of outliving the assumptions that made them acceptable. yaml.load(data) without an explicit Loader is dangerous in large amounts of legacy code that passes Loader=yaml.Loader (the unsafe loader) or uses pre-5.1 versions where the unsafe loader was the default.
Where serialized data enters an application: the attack surface map a pen tester uses
Finding this vulnerability starts with accepting that there is no single endpoint to check. The attack surface is any channel where serialized bytes from an untrusted source eventually reach a deserializer, and that turns out to be a longer list than most engineers expect. HTTP cookies routinely carry serialized session or state objects. HTTP parameters and POST body fields, including hidden form fields that nobody's looked at since the form was built, are another common carrier. API request and response payloads move serialized data between services constantly, often without anyone flagging it as a trust boundary. Message queue payloads moving through Kafka, RabbitMQ, or SQS get consumed by background workers. And, as the ML section makes clear, model files loaded from disk or pulled from remote storage belong on this list too.
Fingerprinting is the first concrete step against all of it. That means scanning HTTP traffic and stored data for the magic byte signatures already covered: AC ED 00 05 or rO0AB for Java, gASV or KGxp for Python pickle, checked before any payload gets sent. Confirming the vulnerability safely comes next, and the standard approach uses out-of-band detection: a payload that only triggers a DNS lookup or an HTTP request to a collaborator server, confirming the application actually deserialized the submitted data without risking any real damage to the target. For Java specifically, ysoserial ships a chain called URLDNS built exactly for this purpose.
Where source code is available, the audit gets more direct. A grep-based sweep for ObjectInputStream, readObject, pickle.loads, and yaml.load calls missing a safe Loader gives a starting inventory, and every hit from that sweep gets manually reviewed, with priority going to any endpoint that touches HTTP parameters, cookies, or message queue payloads. Whitebox access changes what's actually achievable here. A tester holding source code can enumerate every deserialization sink statically and then confirm, one by one, which ones are reachable from untrusted input. A blackbox tester, working with no source access, can only find the sinks that happen to produce some kind of observable behavior from the outside. The map they build is necessarily incomplete compared to what source access reveals. Cached objects read back from Redis, Memcached, or database BLOBs.
How pen testers approach deserialization: from fingerprinting to proved exploit
A rigorous engagement against this vulnerability class follows a fairly fixed sequence, and skipping steps is how testers end up with findings nobody can act on. Reconnaissance comes first: identifying which serialization formats are actually in play by inspecting cookies, request parameters, and inter-service traffic, watching specifically for magic byte signatures and base64 artifacts that give the format away. From there, safe detection takes over. Out-of-band payloads, DNS or HTTP callbacks, confirm that the application is deserializing submitted data at all, without triggering anything destructive in the process. A callback firing is proof the vulnerability class exists, nothing more, nothing less, and that distinction matters for what comes next.
For Java targets specifically, gadget chain identification is its own step. Testers enumerate the libraries sitting on the classpath, often inferred from error messages, response headers, or direct source access, then match those libraries against known chains cataloged in ysoserial or built with custom tooling. Where the classpath is unusual or the libraries aren't well covered by existing chains, static analysis tools like GadgetInspector, or hybrid approaches in the style of ODDFUZZ, help map out candidate chains that haven't been documented yet.
Once a chain or a pickle payload is ready, delivery is next: generate the malicious object, encode it correctly for wherever it's going in, deliver it, and confirm execution through the callback or through a controlled, benign command like a sleep call or a DNS lookup carrying a unique label. Nothing destructive, nothing that risks the target environment, just enough to prove the code ran.
Documentation closes the loop, and this is where sloppy engagements get separated from rigorous ones. A finding that reads "deserialization endpoint detected, potential RCE," with no demonstrated callback and no proof of execution, is an unverified maybe. It's an unverified maybe. If a proved exploit, backed by a callback or a controlled command execution, is present, the report gets prioritized and fixed; if not, it gets shelved because nobody can tell whether the vulnerability can actually be triggered.
WAF evasion adds one more live variable to any engagement working against a defended target. Web application firewalls tend to detect deserialization payloads by matching specific class names embedded inside the serialized data, a signature-based defense that recompiled, renamed gadget chains are built to slip past.
Sources
- Sleeping Giants -- Activating Dormant Java Deserialization Gadget Chains through Stealthy Code Changes
- ODDFUZZ: Discovering Java Deserialization Vulnerabilities via Structure-Aware Directed Greybox Fuzzing
- What is Insecure Deserialization?
- Java deserialization tricks
- Insecure Deserialization in Python - Semgrep
- Exploiting insecure deserialization vulnerabilities | Web Security Academy
- Insecure Deserialization | OWASP Foundation
- An In-depth Study of Java Deserialization Remote-Code Execution Exploits and Vulnerabilities


