Insecure deserialization is a vulnerability that appears when an application rebuilds an object from data it has not verified, letting an attacker decide which classes get instantiated and which methods run during reconstruction. The usual outcome is remote code execution, and it lands before your validation logic reads a single field. The weakness is catalogued as CWE-502 and sits inside the OWASP A08 Software or Data Integrity Failures category. This guide covers how the attack unfolds, which serialization formats carry real risk, what published cases look like, and which defences actually work. The uncomfortable part comes first: your own code is rarely the vulnerable part.

Key Takeaways

  • Insecure deserialization happens when an application reconstructs objects from data an attacker can edit, which hands the attacker control over what the runtime builds and runs.
  • The weakness is tracked as CWE-502 and forms part of the OWASP A08 Software or Data Integrity Failures category.
  • Language-native formats carry the risk: Java ObjectInputStream, Python pickle, PHP unserialize(), .NET BinaryFormatter, and full YAML loaders. Plain JSON carries values only and cannot name a class to instantiate.
  • Exploitation usually rides on gadget chains assembled from library classes already present on the classpath, so auditing your own source code does not remove the exposure.
  • Published cases sit at the top of the severity scale: Fortra GoAnywhere MFT (CVE-2025-10035) was rated CVSS 10.0 and is listed by CISA as used in ransomware campaigns.
  • Only two fixes remove the vulnerability: stop deserializing untrusted objects, or restrict deserialization to an allow-list of expected types.
  • WAF signatures, request rate controls, and edge filtering cut exposure and buy response time, but they do not replace a runtime type filter.

Serialization, Deserialization, and the Gap Between Them

Serialization flattens a live object into a byte sequence that can travel across a network or sit in a cache. Deserialization runs that in reverse and rebuilds the object in memory. The vulnerability lives entirely in the reverse step, because reconstruction is not a passive copy.

To restore an object, a runtime has to pick a class, allocate it, populate its fields and, in most languages, call one or more lifecycle methods on the way. Every one of those decisions is driven by the incoming bytes. The data is not being read. The data is issuing instructions.

That inverts the trust model developers are used to. Normal input validation assumes you receive a value and then decide what to do with it. Here the value decides what gets built, and your validation code runs afterwards, if the process survives long enough to reach it.

“Data which is untrusted cannot be trusted to be well formed.”

Serialized data turns up in far more places than most teams expect:

  • Session cookies, remember-me tokens and view state in hidden form fields
  • API request bodies and message queue payloads in older RPC frameworks
  • Cache entries stored in Redis or Memcached and read back without checks
  • Uploaded files, saved application documents and machine learning model files
  • Licence responses, plugin manifests and inter-service calls inside a cluster

Grouped by behaviour, deserialization belongs with the other application security vulnerabilities that let untrusted input reach a privileged operation. What sets it apart is the payoff. One request, shaped correctly, hands over the whole host rather than one record.

The step that converts that reach into a shell is more mechanical than it sounds.

How Does an Insecure Deserialization Attack Work?

An insecure deserialization attack runs in five steps: locate the serialized blob, identify the format, pick a gadget chain from libraries the target already loads, assemble an object graph that triggers it, then send the result and let the deserializer do the work.

  1. Spot the blob. Any opaque cookie, hidden field, API parameter or cache entry that survives a round trip is a candidate. Base64 hides the shape but not the content.
  2. Fingerprint the format. A stream starting rO0AB is Java. AAEAAAD is .NET. O:4: is PHP. A pickle opcode stream starts with a protocol byte. Each marker names the runtime and the tooling that targets it.
  3. Find a gadget chain. Attackers do not write new exploit code. They look for classes already on the classpath whose reconstruction behaviour can be chained into something useful.
  4. Craft and send. Public generators build the object graph. The payload goes back through the same field the application handed out.
  5. Reconstruction executes. The deserializer instantiates the graph, the chain fires, and the process runs attacker-chosen code with the privileges of the application user.

What separates this from an injection attack against a database is the absence of a parser you control. A SQL payload has to survive a query builder and an escaping layer. A deserialization payload does not fight anything, because the deserializer is behaving exactly as designed.

You might be thinking this all requires a valid login first. Often it does not. The earlier Fortra GoAnywhere flaw (CVE-2023-0669) sat in a licence response servlet reachable before authentication, and Cl0p used it against more than a hundred organisations.

Five-step insecure deserialization attack flow from spotting a serialized blob through to code execution

Never fire a deserialization payload at production. A working proof of concept on a live system is an incident, not a check. Stand up a staging copy pinned to the same dependency versions, because the chain that works depends on the libraries in the build, not on the application code.

Whether any of that is possible in your stack depends less on your framework than on the format your data travels in.

Which Serialization Formats Carry Real Risk?

The risk sits in language-native formats that encode type information, because those formats let the byte stream name the class to instantiate. Formats that carry values only, such as plain JSON, cannot make that request.

The table below maps the dangerous entry point in each runtime to the guardrail the platform gives you and the format you should be moving toward.

Runtime Dangerous entry point Built-in guardrail Safer replacement
Java ObjectInputStream.readObject() ObjectInputFilter allow-list JSON bound to a fixed schema
Python pickle.loads() None, pickle is documented as unsafe json.loads()
PHP unserialize($input) allowed_classes option json_decode()
.NET BinaryFormatter.Deserialize() Removed from the runtime in .NET 9 System.Text.Json
Ruby Marshal.load() None JSON.parse()
YAML (any language) yaml.load() with a full loader Safe loader mode yaml.safe_load()

Microsoft made the position on its own legacy formatter unambiguous when it pulled the implementation out of the runtime.

“BinaryFormatter is an insecure format and the cause of many security bugs.”

JSON deserves a caveat. It is safe as data, not as a container. Libraries that write type metadata into the document, such as polymorphic type handling in Jackson or TypeNameHandling in older Json.NET configurations, reintroduce class instantiation in a text format that looks harmless in a log. XML carries two separate traps of its own: a parser that resolves external entities gives you an XXE attack, and a parser that maps arbitrary types gives you deserialization.

Yes, switching to plain JSON removes the class instantiation problem. But it does not make the payload trustworthy, and the next section shows what attackers do with an object the application believes.

Insecure Deserialization Examples From the Public Record

Published deserialization flaws cluster at the top of the severity scale because a working exploit usually lands as remote code execution, which scores high on confidentiality, integrity and availability at once.

CVE Product What gets deserialized Severity Status
CVE-2025-10035 Fortra GoAnywhere MFT A forged license response object in the License Servlet CVSS 10.0 CISA KEV, linked to ransomware
CVE-2025-49113 Roundcube Webmail An unvalidated _from parameter in the settings upload handler CVSS 9.9 CISA KEV
CVE-2025-40551 SolarWinds Web Help Desk Untrusted data on an unauthenticated request path CVSS 9.8 CISA KEV
CVE-2019-18935 Progress Telerik UI for ASP.NET AJAX A .NET object passed to the RadAsyncUpload handler CVSS 9.8 CISA KEV
CVE-2026-45659 Microsoft SharePoint Server An object reachable by any authenticated user CVSS 8.8 CISA KEV

A pattern runs through those entries. None of them needed a novel technique. Each exposed a deserialization sink on a network-reachable path, and each was weaponised quickly once the details were public. The Roundcube flaw had been sitting in the codebase for more than ten years, and researchers reported working exploit code circulating within days of disclosure.

Here is what most teams miss: code age is not the risk factor. Exposure begins the day the endpoint becomes reachable, not the day the code is written. A ten-year-old parser is safe until someone points a scanner at it, and then it is a zero-day.

Not every case ends in code execution either. When the serialized object carries authorization state, a tampered role or tenant field turns the same flaw into broken access control, and the application never objects because the object it received is structurally valid.

The reason these bugs survive code review is that the dangerous part is not in the code being reviewed.

Gadget Chains and Why Auditing Your Own Code Is Not Enough

A gadget chain is a sequence of method calls, each belonging to a class already present in your application’s dependencies, that the deserializer triggers as it rebuilds an object graph. Your application never calls a command execution function. The chain does it on your behalf.

In Java the canonical example runs through Apache Commons Collections. Rebuilding a map forces a hash calculation, which resolves through a lazily populated map, which invokes a transformer chain, which reaches a reflective call into the runtime’s command execution API. Every link is ordinary library code doing precisely what it was written to do.

Public tooling ships these chains ready-made. The ysoserial project catalogues Java payloads across dozens of libraries, and phpggc does the same job for PHP frameworks, which is why exploitation rarely requires original research.

// Vulnerable: the byte stream decides which class gets built ObjectInputStream in = new ObjectInputStream(request.getInputStream()); Order order = (Order) in.readObject(); // Fixed: an allow-list rejects every type you did not expect ObjectInputStream in = new ObjectInputStream(request.getInputStream()); in.setObjectInputFilter(ObjectInputFilter.Config.createFilter( “com.example.model.Order;com.example.model.Item;!*”)); Order order = (Order) in.readObject();

The filter in that second example comes from the platform itself. JEP 290 added serialization filtering to Java and it was backported to earlier update releases, so an allow-list is available on almost any supported runtime. An allow-list beats a blocklist here for the same reason it does everywhere: you can enumerate the five classes you expect, and nobody can enumerate every class an attacker might reach.

How to Prevent Insecure Deserialization

Insecure deserialization prevention works in four layers, and only the top two remove the vulnerability. The rest reduce exposure and buy time, which matters, but they are not a fix.

Four layers of insecure deserialization prevention ranked from design changes and runtime type filters to edge controls

  1. Stop deserializing untrusted objects. Move state to plain data structures with a fixed schema, or keep it server side behind an opaque session identifier. A payload that names no classes cannot instantiate any.
  2. Enforce a runtime type allow-list. Set an ObjectInputFilter in Java, pass allowed_classes to unserialize in PHP, use a safe loader for YAML, and treat pickle as unusable for anything a user can touch.
  3. Verify integrity before parsing. Compute an HMAC over the payload with a server-held key, compare it in constant time, and reject the request before any decode step. Signing after the decode protects nothing.
  4. Contain the blast radius. Run deserialization in a low-privilege process, block outbound connections it does not need, and log every deserialization exception, because failed attempts throw before successful ones land.

// PHP: refuse every class instead of trusting the stream $data = unserialize($input, [‘allowed_classes’ => false]); # Python: pickle has no safe mode, so keep it away from user input import json payload = json.loads(request.body) # correct # payload = pickle.loads(request.body) # remote code execution

Network segmentation belongs underneath all of that. A firewall solution decides which hosts can reach a service in the first place, which shrinks the population of clients able to deliver a payload. It does nothing about a request that is already permitted, which is why the application layer question comes next.

So can anything at the edge help, or is this purely a code problem?

Can a WAF Stop Insecure Deserialization?

A WAF blocks a large share of known deserialization payloads by matching the signatures serialized streams leave behind, but it cannot judge whether an object graph is safe, because that decision happens inside your runtime after the request is allowed through.

Pattern recognition at scale is what edge inspection does well. An advanced web application firewall can flag base64 that decodes to a Java stream header, catch a PHP object marker in a form field, and score requests that carry serialized markers on endpoints that have no business receiving them. That covers the automated scanning that follows every public disclosure.

Managed rule sets already cover this ground. The OWASP Core Rule Set reserves its 944 rule range for Java attack patterns, including deserialization signatures, alongside separate ranges for injection and file inclusion. Running those rules in detection mode gives you a map of which endpoints are being probed before you commit to blocking.

Two supporting controls matter more than they look. Finding a working chain takes iteration, so rate limiting on the endpoints that accept opaque tokens turns a fast automated search into a slow one that your monitoring can catch. And management interfaces, licence endpoints and admin uploads rarely need to accept traffic from the whole internet, so Custom IP Lists remove most of the attack population before inspection even starts.

The honest comparison looks like this.

Control Known payload signatures Novel gadget chains Effort Where it runs
WAF signature rules Blocks most Usually misses Low Edge
Request rate controls Slows discovery Slows discovery Low Edge
Runtime type allow-list Blocks Blocks Medium Application
Removing the deserialization sink Blocks Blocks High Architecture

Where Edge Controls Help and Where They Stop

An edge tier terminates connections before they reach your origin, which gives you one place to apply signatures, throttles and allow-lists across every application behind it. A secure CDN also removes direct origin reachability, so an attacker cannot skip the inspection layer by connecting to the backend address.

Layered together as global edge security, those controls shorten the window between a disclosure and your patch. They do not close it. Treat every edge rule as a mitigation with an expiry date, and keep the runtime fix on the sprint board.

Switch WAF deserialization rules on in detection mode first. Java and .NET stacks sometimes move legitimate serialized state between services, and a blanket block on stream markers can take out your own integrations before it takes out an attacker.

None of that helps if you cannot say where your own deserialization sinks are.

How to Test Your Application for Deserialization Flaws

Testing for deserialization flaws means finding the sinks first, then proving which of them are reachable from untrusted input. Scanners find the obvious cases, and code inventory finds the rest.

  1. Inventory the sinks. Search the codebase for readObject, unserialize, pickle.loads, yaml.load, BinaryFormatter, Marshal.load, and their framework wrappers. Record every hit with the file and the caller.
  2. Trace the sources. For each sink, work backwards to the request parameter, header, file upload or cache key that feeds it. A sink with no untrusted source is a note, not a finding.
  3. Decode every opaque token. Take each cookie and hidden field the application issues, base64 decode it, and look at the first bytes. If a stream marker appears, the application is round-tripping objects.
  4. Scan the whole dependency tree. Software composition analysis tells you which gadget-bearing libraries a build actually ships, including the transitive ones nobody on the team chose.
  5. Probe in staging. Generate payloads with ysoserial or phpggc against a copy pinned to production dependency versions, and watch for out-of-band callbacks rather than relying on response bodies.
  6. Write the regression test. Once a filter is in place, add a test that sends an unexpected type and asserts the request is rejected, so the allow-list survives the next refactor.

The OWASP Deserialization Cheat Sheet keeps language-specific guidance current, and the CISA Known Exploited Vulnerabilities catalogue is worth filtering for CWE-502 entries whenever you inherit a product you did not build.

Final Thought on Insecure Deserialization

Insecure deserialization is unusual among web vulnerabilities because the defect is not really in the code that gets audited. It is in a design decision: the choice to accept a serialized object from somewhere you do not control and rebuild it faithfully. Once you frame it that way, the fix stops being a patch and becomes a boundary. Data crossing into your application should be data, not a set of instructions about which objects to construct.

Practically, that means two moves. Take an inventory of every deserialization sink you own and the untrusted paths that reach them, and put a type allow-list in front of the ones you cannot remove yet. Edge filtering, request throttles and managed rule sets are worth running while that work happens, because they shorten the window when the next disclosure lands. They are the ambulance, not the seatbelt.

Common Questions About Insecure Deserialization

Is JSON completely safe from deserialization attacks?

Plain JSON is safe from class instantiation because it carries values only, with no type information the parser can act on. The risk returns when a library is configured to embed type hints in the document, which lets the payload name a class again. Bind JSON to a fixed schema or concrete type rather than to a generic object map.

Does signing the serialized payload make deserialization safe?

Signing helps only if the signature is verified before anything is decoded, and only for as long as the key stays secret. If the key leaks, is guessable, or lives in client-side code, the attacker signs their own payload and the check passes. Treat integrity verification as a control that stops tampering, not one that makes an unsafe format safe.

Can insecure deserialization be exploited without a valid login?

Yes, whenever the vulnerable sink sits on a pre-authentication path. License handlers, upload endpoints, error pages, and health check interfaces have all carried unauthenticated deserialization flaws. Authentication reduces the attacker population, so it is worth having, but it is not a mitigation you can rely on.