Insecure Deserialization Exploits in Java Spring Applications
Attackers exploit Java deserialization through gadget chains already present in your dependencies.

Insecure deserialization in Java Spring applications follows a pattern that's learnable, repeatable, and, once you understand it, kind of obvious in hindsight. Untrusted bytes go in, a gadget chain does the heavy lifting, and remote code execution comes out the other end. Knowing that full chain, start to finish, is the only way to actually tell whether your application is exposed or just looks fine on paper.
Start with the basics. Serialization takes a live Java object and turns it into a byte stream so it can be stored or shipped somewhere else. Deserialization does the reverse: it takes those bytes and rebuilds the object. Simple enough. The rebuilding step is dynamic: different classes produce different runtime behavior once reconstructed. Different classes produce different runtime behavior once reconstructed, and Java's reflection API means the deserializer can call methods it never saw at compile time. That dynamism is a feature when everything on the classpath is trusted. It's a weapon when it isn't.
Here's the actual problem: deserialization accepts bytes that came from outside the application and turns them into live, running objects. If nothing checks those bytes before they get reconstructed, the pipeline has handed a blank check to whoever supplied them. That's not a bug in one library or one line of code. It's structural. The interface was built years ago for interoperability between runtime environments, not to police trust boundaries. It has no concept of "this byte stream came from someone I don't know."
Gadget chains are what make this exploitable in practice. A gadget chain is a sequence of methods that already exist on the classpath, legitimate code, nothing malicious added, that fire off in sequence during deserialization of a crafted payload. No new binary gets dropped. No custom exploit code gets injected. The attacker just finds a path through methods your own dependencies already ship, and rides it to a dangerous outcome. That's the part that makes this attack class so hard to reason about defensively: the richer your dependency tree, the more raw material an attacker has to work with. A minimal classpath with a handful of libraries gives an attacker almost nothing. An enterprise Spring app pulling in dozens of transitive dependencies gives them a menu.
MITRE's formal definition, under CWE-502, puts it this way: the product deserializes untrusted data without sufficiently ensuring the resulting data will be valid. That's the textbook framing, and everything below builds on it. MITRE's EMB3D threat model even carries a dedicated entry, TID-326, for insecure deserialization in embedded devices used in critical infrastructure, so this isn't a web-app-only concern. And the scale matters here specifically because of the framework in question: Spring commands over 22% of the web framework market, according to ZeroPath's analysis. When a vulnerability class this structural meets a framework this widely deployed, the exposure isn't theoretical.
The attack sequence: from crafted input to remote code execution
The attack starts with reconnaissance, and it's not glamorous. An attacker looks for a deserializing endpoint. In messaging-based systems, that means checking whether the target uses a converter class known to deserialize JSON messages into Java objects, and whether the message broker itself is reachable from outside the application's trust boundary. If the broker is exposed, or reachable through a compromised adjacent system, the attacker doesn't need credentials to the Spring app.
Next comes classpath enumeration. The attacker figures out which gadget-eligible libraries are present, because the gadget chain has to be built from parts that actually exist on the target. Enterprise Spring deployments tend to carry heavy dependency sets, and every added library is a potential source of new gadgets. Because of this, the size of the target's dependency tree is not just a code-quality footnote; every added library expands the attack surface.
Then payload construction. The attacker crafts a message carrying a type identifier, in JMS this is often the _type or typeId header, that points at a known gadget class. No login required, no session token, nothing that resembles authentication to the Spring application itself.
The message gets delivered: published to a queue or topic that the vulnerable application consumes. Once the app picks it up, the converter reads that typeId, calls ClassUtils.forName() with the attacker's chosen class name, and instantiates it. That instantiation is the trigger. From there, a chain of method calls fires automatically, self-executing during the deserialization process, working toward a sink like Method.invoke(). That's the moment code execution becomes real, whether the end goal is remote code execution, data theft, or just knocking the service offline.
The root fix, and this matters because it explains why so many patches in this space look identical, is an allowlist. Without one, there's no check on which class gets loaded, and Java's runtime polymorphism means any reachable, overridden method sitting on the classpath is fair game as a gadget. Compare Android's classpath, which typically lacks libraries like commons-collections or spring-beans, to a server-side Spring deployment carrying exactly those libraries. The difference in gadget graph size is enormous, and it's not an accident: it's a direct function of what's installed.
Even dedicated tooling struggles to find every path. Research from Cao et al. on a tool called ODDFUZZ found 16 of the 34 known gadget chains in the ysoserial toolkit, where state-of-the-art baseline tools identified only three between them. That gap says something uncomfortable: a lot of exploitable paths sit undetected even when people are actively looking.
CVE-2026-41855: how this attack pattern plays out in a current Spring JMS vulnerability
CVE-2026-41855 is this exact pattern, disclosed by the Spring team on June 8, 2026, after a report from external researcher wo1enca1ca1. It hits Spring Framework's JMS message converters directly.
The root cause, per ZeroPath's writeup, is almost embarrassingly simple to state: MappingJackson2MessageConverter and JacksonJsonMessageConverter pulled the Java type straight from the inbound message's typeId property and passed it to ClassUtils.forName(). No allowlist. No blocklist. No validation of any kind before the class got loaded.
CVSS scored it 8.1, high but not critical, with the vector AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H. The vector tells you what has to be true for this to bite:
AV:Nmeans it's exploitable remotely over the network, through the JMS broker.PR:Nmeans no authentication to the Spring app is needed, just the ability to publish a message to a destination the app consumes.AC:His why this is 8.1 instead of a 9-plus critical score: exploitation needs both an untrusted JMS environment and a usable gadget class already sitting on the target's classpath. Neither condition is guaranteed everywhere.C:H/I:H/A:Hmeans that once both conditions are true, the blast radius covers confidentiality, integrity, and availability, all three, fully.
Affected versions, per HeroDevs' rundown, span Spring Framework 5.3.0 through 5.3.48, 6.1.0 through 6.1.27, 6.2.0 through 6.2.18, and 7.0.0 through 7.0.7. That's a wide net across four major lines.
Patches landed across all four branches, though the terms differ significantly by branch, which matters a lot for anyone triaging this. The 7.0.x line got fixed in 7.0.8 for open source and 7.0.7.1 for commercial customers. The 6.2.x line got 6.2.19 (OSS) and 6.2.18.1 (commercial). But 6.1.x only got a fix at 6.1.28, and it's commercial-only, because 6.1.x hit end of open-source life back on June 30, 2025. Same story for 5.3.x: the fix landed at 5.3.49, commercial-only, since that branch went EOL for open source on August 31, 2024. Anyone still running those older branches without a commercial support contract has no free upstream patch waiting for them. The choices are migrating to a supported branch or paying for a commercially maintained build.
What does the actual fix look like in the code? ZeroPath's analysis of the fix commit (9bec52b1) shows 119 lines added and zero deleted, introducing a new trustedPackages field and setter on the affected converters. That's the whole story: an allowlist bolted onto the type resolution step, checked before the class gets loaded rather than after. It's the textbook structural defense described above, implemented almost exactly as the theory predicts.
This isn't a one-off. JMS has been a soft spot across Spring's history, appearing repeatedly in its record of vulnerabilities. CVE-2026-40860 involved a separate deserialization flaw where JMS ObjectMessage payloads got pulled through javax.jms.ObjectMessage.getObject() with no ObjectInputFilter, no allowlist, no denylist. CVE-2026-27830 hit c3p0's unsafe deserialization through userOverridesAsString, leading to RCE. Different libraries, same root failure: untrusted bytes reaching a deserializer with nothing standing guard.
Why gadget chains change: supply chain shifts and dormant chains
Patching CVE-2026-41855 fixes one known vector. It does not fix the underlying classpath, and most teams wrongly assume that patching one vector is enough.
Researchers at Umeå University (Kreyssig et al.) studied whether a class's serializability, meaning whether it's a candidate for gadget chain participation, stays constant over a library's lifetime. It doesn't stay constant: serializability can change across a library's versions. Their study covered 1,475 widely used Maven dependencies across 111,275 versions, and they zeroed in on 533 dependencies that had at least one serializable class. They then applied three modification patterns meant to mimic stealthy, plausible code changes: adding the Serializable interface to a concrete class, and making a superclass or interface serializable in a way that transitively affects every subtype below it, easy to miss in a code review, since nobody's re-auditing every subclass every time a parent interface changes.
The results are stark. Applying those patterns activated new gadget chain detections in 26.08% of the 533 dependencies tested. Manual verification confirmed dormant, real gadget chains in 53 of them. And 49.06% of confirmed true positives needed only one of the three patterns to activate, so accidentally (or deliberately) introducing a new gadget takes very little. A single interface change in a transitive dependency, something a maintainer might not even flag in a changelog, can quietly reopen an attack path that a security review closed months earlier.
This has a direct supply chain consequence: even if one dependency's build pipeline runs gadget chain detection, a chain can still show up through gadgets sitting in some other dependency nobody's watching as closely. Detection right now is reactive. Nobody's predicting which library update will flip a dormant gadget live.
And the reference tooling hasn't kept pace. Ysoserial, the standard toolkit most people cite when they talk about Java gadget chains, last added a new chain back in February 2021. Since 2020, 55 new critical CWE-502 vulnerabilities have landed in the National Vulnerability Database. The tooling most teams lean on for reference is years behind the vulnerabilities actually showing up.
For Spring teams specifically, the takeaway is uncomfortable but simple: patching CVE-2026-41855 closes that door. It says nothing about tomorrow's dependency bump, which might reopen a different one, without a single line of application code changing.
Historical precedents that show what exploitation at scale looks like
None of this is new, and the history shows what happens when this vulnerability class meets scale.
Jenkins lived through what got nicknamed the "Jenkinspocalypse," a run of three deserialization CVEs. CVE-2015-8103 exposed a remotely reachable deserialization endpoint that allowed unauthenticated code execution. CVE-2016-0792 went through XStream, using a crafted XML file and a Groovy MethodClosure gadget rather than CommonsCollections. CVE-2016-9299 hit the CLI again, this time through an LDAP-based second-stage chain. Publicly reachable Jenkins instances were exploited broadly, and the common thread across all three CVEs was the same: unauthenticated deserialization endpoints, different gadget chains, same underlying failure.
Oracle WebLogic tells a longer, uglier story: a continuous stream of deserialization RCEs spanning many years. Each patch closed one specific reachable deserializer, only for a new entry point to appear in the reachable code paths. That saga is the clearest illustration available of what happens when a team treats deserialization as a series of individual bugs to patch rather than an architectural trust boundary to enforce. You cannot patch your way out of a structural problem one CVE at a time.
Jackson-databind had its own moment with CVE-2020-24616, an RCE built on a gadget chain exploiting the same polymorphic deserialization mechanism, using the same polymorphic deserialization mechanism that underlies CVE-2026-41855 today. Same mechanism, different decade, different dependency.
And the timeline for weaponization has compressed hard. CVE-2025-24813 saw active exploitation roughly 30 hours after public disclosure, according to Endor Labs. That's not a window measured in weeks anymore. It's measured in a single business day.
Every one of these cases shares the same skeleton: untrusted input reaches a deserializer with no type restriction, the classpath supplies the gadget, and the damage scales with how many instances sit reachable on the internet. A penetration test run last quarter, before a dependency update shipped or before someone widened a broker's trust boundary, doesn't necessarily describe this quarter's exposure. The attack surface moves even when the application code doesn't.
Testing methodology and tooling for finding deserialization vulnerabilities in a Spring application
Finding these issues starts with mapping every place untrusted data can reach a deserializer. That means JMS consumers, REST endpoints that accept serialized objects, RMI endpoints, any custom ObjectInputStream subclass, and third-party libraries that quietly deserialize data internally without the application team even realizing it.
From there, classpath analysis: enumerate every library present and check it against known gadget-eligible dependencies, commons-collections, spring-beans, commons-dbcp2, and similar names that recur repeatedly in gadget chain research. The gadget graph is entirely a function of what's actually installed, so this step isn't optional groundwork, it's the core of the assessment.
Directed greybox fuzzing has proven effective here. The ODDFUZZ approach, mentioned earlier, generates structurally valid serialized objects as fuzzing seeds and uses hybrid feedback to steer the fuzzer toward actual deserialization sinks rather than wandering randomly. Beyond rediscovering 16 of the 34 known ysoserial chains, that research turned up six previously unreported exploitable chains across Oracle WebLogic Server, Apache Dubbo, Sonatype Nexus, and protostuff, with five CVEs assigned as a result. That's not a small haul for one research effort.
Whitebox access matters enormously here. Working from source code and dependency manifests like pom.xml or build.gradle lets a tester enumerate the classpath precisely and trace taint sources accurately. Someone testing from outside, with no source access, can only watch how the application behaves. They can't see the full gadget graph beneath it, because that graph is produced by every library on the classpath, and testers without source access can only observe the parts that surface in observable behavior.
None of this replaces human judgment, though. Automated tools surface candidates, not confirmed findings. A person still has to construct or validate a working gadget chain before calling something exploitable. Until that happens, it's a hypothesis, not a finding, and treating it otherwise inflates a report with noise. A credible finding documents the exact crafted payload, the type identifier used, the specific gadget chain traversed step by step, and the impact actually achieved, not a scanner alert citing CWE-502 with nothing behind it.
For the CVE-2026-41855 pattern specifically, the checklist is short: confirm whether MappingJackson2MessageConverter or JacksonJsonMessageConverter is configured, confirm whether typeId resolution is actually constrained by a trustedPackages allowlist, and confirm the trust boundaries around the message broker itself.
And retesting after a patch is essential here, more so than in most vulnerability classes, because the gadget graph shifts with every dependency update. Because the gadget graph shifts with every dependency update, a retest after remediation should confirm that the specific chain used in testing is now closed, not just that a code change got merged and deployed.
Remediating deserialization exposure: what effective fixes look like versus cosmetic ones
The strongest fix is removing the deserializer entirely, where that's architecturally possible. If native Java serialization or Jackson's polymorphic deserialization isn't actually required by the design, swapping it for a data-only format, JSON validated against a schema, Protobuf, Avro, closes the door outright, because none of those formats instantiate arbitrary classes on the way in. There's nothing for a gadget chain to grab onto.
Where the deserializer has to stay, enforcing a type allowlist is the next-best structural fix, which is what Spring's own patch for CVE-2026-41855 does. Calling setTrustedPackages() on MappingJackson2MessageConverter or JacksonJsonMessageConverter restricts instantiatable types to a known-safe set. Leaving that field null leaves the door unlocked, no matter what version number is in the pom file.
Beyond configuration, Java itself has moved toward addressing this at the platform level. JEP 290 introduced a filtering mechanism for incoming serialization data, and JEP 415 built on it with context-specific deserialization filters, letting an application define, ahead of time, exactly which classes are allowed to come back to life from a byte stream. These are platform-level controls, not application patches, and they matter because they don't depend on every library author remembering to add their own check.
Cosmetic fixes look different and are recognizable as such. Wrapping a deserialization call in a broad try-catch block doesn't stop a gadget chain, it just hides the exception after the damage during execution has already happened. Logging the typeId without validating it against anything documents the attack after the fact rather than preventing it. Upgrading a library version without checking whether the new version still carries the vulnerable code path assumes the patch notes told the whole story, and as CVE-2026-41855's uneven rollout across Spring branches shows, sometimes the fix simply isn't available yet for the branch in production.
The dividing line is simple, even if it isn't easy: a real fix stops the class from loading. Everything else just reacts to what already happened.

