Est.

Insecure Deserialization Vulnerabilities in Java and Node.js SaaS Applications

Attacks hide in how Java and Node.js rebuild objects from untrusted data.

Senior Writer · · 11 min read · Updated
Cover illustration for “Insecure Deserialization Vulnerabilities in Java and Node.js SaaS Applications”
Verified Vulnerabilities & Exploits · August 25, 2026 · 11 min read · 2,503 words

Insecure deserialization is a mechanical problem before it's ever a policy problem. Java's ObjectInputStream and Node's various unserialize functions rebuild objects from raw bytes the same way every time. Once you know the steps a runtime takes to reconstruct an object, you know exactly where an exploit gets its foothold. I've spent enough time chasing these bugs through client codebases to have opinions about where they hide, so let me walk through the mechanism in both ecosystems, then map it onto real SaaS architecture: where it gets in, how you catch it, what actually closes the door.

Serialization is a contract. Your app writes an object to a byte stream and trusts that reading the stream back later gives it the same object, same fields, same type. That trust is the entire reason the feature exists.

The contract breaks the moment the byte stream comes from somewhere you don't control. The runtime builds whatever the stream describes, without regard for what your application expected to get back. Describe a chain of method calls ending in a shell command, and the runtime builds that chain. That's its job, and it does the job well regardless of who's asking.

What makes this category nasty is that execution happens during reconstruction, before any of your application code gets a look at what came back. There's no hook to say "hold on, check this first." The object gets built, and building the object often is the attack. By the time your validation logic runs, you've already lost the game.

The damage covers the whole spectrum: remote code execution, authentication bypass, privilege escalation, denial of service. None of it needs a second bug. One bad deserialization call is enough on its own, which is part of why I've never liked how quietly this class of vulnerability sits in most risk registers.

The risk doesn't fade with better coding habits, because serialization is baked into the runtime itself: Java's ObjectInputStream, Node's JSON handling, the pile of serialization packages sitting around it. The attack surface follows the runtime, regardless of whether some developer remembered to bolt on a check. OWASP folded this into Top 10:2025 under A08, Software and Data Integrity Failures, if you need a name for it in a compliance meeting.

How Java deserialization gadget chains turn a library into a weapon

Every Java deserialization exploit starts at the same door: ObjectInputStream.readObject() taking bytes from somewhere untrusted. Could be a network socket, a session cookie, an inter-service call, a message queue, a file some user uploaded. The vulnerability lives in what happens after the method accepts those bytes.

A gadget chain is a sequence of method calls that already exist, sitting in libraries already loaded on the JVM's classpath. The attacker doesn't write new code; they write a payload that walks through methods your app already has lying around and steers that walk toward something dangerous, whether that's exec(), a file write, or a class loader trick.

The trap is structural. The JVM builds the entire object graph before any type check fires. CVE-2024-45772 in Apache Lucene shows this cleanly: the runtime reconstructed the attacker's object from the stream, and only afterward did the application try to cast it to Throwable. By then the cast was pointless. The object already existed, and existing was the exploit.

Ysoserial turned this from a research curiosity into a one-command tool. Feed it a command and the name of a gadget chain, and it spits out a serialized payload, no exploit-dev skill required beyond finding an endpoint with the right library on the classpath. The original chains targeted Apache Commons Collections 3.x and 4.x, Spring Beans and Core 4.x, Groovy 2.3.x. The tool's grown since, covering Commons BeanUtils and JRE builds at or below 1.7u21, among others.

Take CommonsCollections1. It routes through AnnotationInvocationHandler, into a Proxy, into a LazyMap, into a ChainedTransformer holding an array of Transformer objects. Every field in that chain sits right there in the byte stream, attacker-controlled, waiting for the JVM to walk it. I still find it a little unsettling how elegant that chain is, honestly, since nothing about it looks like malware until you know what you're looking at.

Your application never has to call the vulnerable methods directly, and that's the part that surprises people every time I explain it. The library just has to sit on the classpath, unused by your own code, invisible to your own code review. When Apache Commons Collections got disclosed back in 2015, it hit WebLogic, WebSphere, JBoss, and Jenkins all at once, because they all happened to ship the same vulnerable library buried somewhere in a dependency tree.

This isn't a museum piece from a decade-old disclosure, either. CVE-2025-20124, found in Cisco Identity Services Engine, is a Java deserialization bug with a CVSS score of 9.1, exploitable by any valid admin account, including read-only ones. Gadget chains are a live, current-year problem in enterprise software, and anyone treating them as a 2015 relic is due for a rude surprise.

How node-serialize exploits IIFE execution in Node.js

Node's version of this bug looks different on the surface but runs on the same logic underneath. The node-serialize package, versions 0.0.4 and below, ships an unserialize() function that rebuilds JavaScript objects from a JSON-like string, with no check on that string first.

The attack rides in on an immediately invoked function expression. An attacker wraps a JavaScript expression in an IIFE and drops it inside the serialized string. When unserialize() processes that string, the runtime evaluates the function as part of rebuilding the object, with no separate "should I run this" step; the function just runs.

The payload sits inside a property value in the serialized object, and unserialize() treats that function as an object definition to build, which means running it.

The package is unmaintained, so no patch is coming. Fixing this means ripping the dependency out, not waiting on a version bump that isn't showing up.

The pattern reaches further than this one package, though. Any Node.js code that calls eval() on deserialized content, hands untrusted input to vm.runInNewContext(), or loads YAML with an unsafe parser instead of a safe one (the load versus safeLoad split in js-yaml is the textbook example) rebuilds the same shape of bug from different parts.

SaaS teams tend to underrate this because JSON.parse() itself is genuinely safe; it just parses data and doesn't execute anything. The risk shows up one step later, in what the app does with the object JSON.parse() handed back. Pass that object into eval, into a template engine, into a dynamic require() call, and you've rebuilt the vulnerability from scratch, whether or not node-serialize shows up anywhere in your package.json.

Where these vulnerabilities actually enter a SaaS application

Venn diagram: Java vs Node.js Deserialization Vulnerabilities. Compares Java and Node.js; overlap: Shared Risk.

On the Java side, a handful of spots deserve a hard look every single time. Session and cookie storage tops the list: anything that serializes a session object into a cookie or into Redis and pulls it back out on the next request. Java RMI endpoints and JMX management interfaces come next, often left wide open on internal networks and skipped in threat modeling because "internal" quietly gets treated as "safe." Message queue consumers for Kafka, RabbitMQ, or ActiveMQ are worth checking too, specifically whether the consumer calls readObject() directly on the message body.

Caching layers matter as well: serialized objects going into Memcached or Redis and coming back out. Inter-service APIs that pass raw Java objects between microservices instead of plain JSON deserve the same scrutiny, and so do file import features that accept configuration files or data exports and deserialize them on the way in.

Node has its own list. Check package.json and the full transitive dependency tree for node-serialize, since it can arrive through a dependency of a dependency without anyone choosing it on purpose. Cookie parsing middleware that reconstructs objects from cookie values, signed or not, is another spot. WebSocket handlers parsing structured payloads and passing the result downstream need a look too, along with YAML config loading through an unsafe loader variant, and template engines fed user-controlled data that eventually reaches eval() or a Function() constructor.

There's a supply chain angle underneath all of this that a grep-based audit can't touch. A library your code never calls directly can still deserialize data on your behalf, three or four layers deep in a tree nobody's read. Source code and dependency manifest access finds what black-box scanning misses for exactly this reason. A scanner sees an endpoint responding to a request; source access shows what that endpoint actually does with the input once it lands.

Detection approaches and what each one actually catches

Static analysis finds the obvious stuff in source: ObjectInputStream.readObject() calls, node-serialize imports, unsafe YAML loader use. High signal for the patterns it knows to look for, but it goes blind the moment a gadget chain gets assembled through transitive dependencies or reflection, since none of that shows up as a direct call in the code SAST is reading.

Software composition analysis catches a different slice: known-vulnerable library versions sitting in a dependency manifest, Commons Collections, node-serialize, whatever's picked up a CVE number. That's the "library on the classpath" problem that makes ysoserial work in the first place. But SCA only flags versions already tagged, so anything novel walks right past it.

Dynamic testing, the pen test approach, actually fires a ysoserial payload at a live endpoint. It's the only method here that confirms exploitability instead of guessing at it from a pattern match. A finding here is proven, not inferred, and I trust proven over inferred every single time someone hands me a scan report and calls it done.

Runtime application self-protection instruments the JVM or Node runtime directly, catching deserialization calls as they happen. It can flag gadget chains SAST never saw coming, since it watches behavior instead of matching signatures. That comes at a cost, though: added latency and operational overhead that not every team wants sitting in production.

All four share the same blind spot. None of them can reliably tell you whether attacker-controlled data actually reaches the deserialization call. That takes tracing the data flow by hand, from the point it enters the system to the moment it hits ObjectInputStream or unserialize(), and that's a job for a person, not a tool, no matter how mature the tooling gets.

Pull request scanning deserves a mention too, since it catches new deserialization patterns before they ever ship. The earlier a finding turns up in the pipeline, the cheaper the fix.

Remediation ranked by how completely each control eliminates the risk

Diagram: Remediation Tiers: From Eliminating the Risk to Limiting the Blast Radius. Visualizes: Show five ranked remediation tiers as a vertical stepped list or priority ladder, each labeled with its core control and a critical caveat.Diagram: Remediation Tiers: From Root Cause Elimination to Blast-Radius Control. Visualizes: Visualize five ranked remediation tiers for insecure deserialization, ordered by how completely each one eliminates risk.

Tier 1 kills the vulnerability class outright: stop deserializing untrusted data with native mechanisms. In Java, that means swapping ObjectInputStream for data-only formats, Jackson or Gson for JSON, Protocol Buffers or Avro for anything crossing a trust boundary. In Node, it means pulling node-serialize and using JSON.parse() for plain data, or a maintained, schema-validated library if you genuinely need structured object transfer. This is the only tier that makes gadget chains irrelevant, since no deserialization call means no door for a chain to walk through.

Tier 2 adds cryptographic integrity: HMAC-sign the payload before it goes out, check the signature before calling readObject() or unserialize() on the way back in. It stops tampering, not exploitation, once the signing key leaks. The SharePoint ToolShell chain, CVE-2025-53770 paired with CVE-2025-53771, shows exactly how this fails in the wild. An attacker who grabs the ValidationKey defeats the HMAC completely, and the signature stops meaning a thing.

Tier 3 is Java-specific: class allowlisting through JEP 290. Serialization filters let the JVM reject any class not on an explicit list, rejecting before the object ever gets constructed. That requires knowing, precisely, which classes your app legitimately deserializes, which makes it a decent stopgap for legacy systems that can't jump to Tier 1 right away. It won't save you, though, if one of the classes you allowed turns out to be a gadget entry point itself.

Tier 4 limits blast radius rather than blocking the initial hit: run deserialization in a sandboxed process with seccomp or AppArmor profiles denying exec() and file-write syscalls, and segment the network so a compromised deserializer can't reach the database or the metadata service. A contained incident beats a full breach every time, even when the exploit still lands.

Tier 5 is logging and monitoring, defense in depth rather than prevention. Log every deserialization entry point, and alert on unexpected class names, odd payload sizes, deserialization calls arriving from unauthenticated contexts. Treat those anomalies as signals worth chasing, not noise, because a gadget chain will often leave a recognizable class name sitting in the logs before it fully lands.

For legacy Java systems that can't jump straight to Tier 1, stacking Tiers 2 through 4 gives real, layered protection while a proper migration gets planned. Treat it as a bridge, not a home; I've seen too many teams treat the bridge as the destination and never come back to finish the job.

Why a penetration test finds deserialization issues that internal review misses

Developers know what their serialization code is supposed to receive. That's the blind spot, in one sentence, because they built it to handle a specific shape of input, and threat modeling done by the same team inherits that same assumption. Nobody sits down and models what happens when an attacker hands the endpoint a ysoserial payload or an IIFE stuffed into a cookie value.

Whitebox penetration testing closes that gap by giving a tester source access, so they can trace every path from network input to ObjectInputStream, including paths running through third-party libraries nobody on the internal team ever thought to check. Black-box scanning, by contrast, throws payloads at visible endpoints and misses internal deserialization calls that never surface in an HTTP response.

A working exploit is what separates a real finding from a guess. "ObjectInputStream gets called somewhere near untrusted input" is a hypothesis, while a working ysoserial payload that achieves remote code execution on the test environment is proof, and only proof tells a team the risk is real and the chain runs all the way through.

Retesting matters just as much once the fix lands. Swapping ObjectInputStream for Jackson is a real, meaningful change, but it needs verification. A retest confirms the original entry point is actually gone, and that no second, parallel deserialization path slipped through unnoticed.

There's a compliance angle too. SOC 2, HIPAA, and ISO 27001 assessors want evidence of real vulnerability discovery, not a scanner export with a pass-fail stamp slapped on it. A pen test report from an OSCP-certified reviewer, documenting working exploits, clears an evidentiary bar that automated tooling alone can't reach.

Cadence matters more than most teams give it credit for. One annual test is a single snapshot in time, but a deserialization bug can walk in through a routine dependency upgrade on some random Tuesday afternoon. Continuous pull request scanning catches that exact moment: when the vulnerable library version lands in the manifest, before it ever ships to production.

Sources

  1. invicti.com
  2. cisco.com
  3. cyserch.com

More in Verified Vulnerabilities & Exploits