Safeguard
Application Security•

CWE-502 Deserialization of Untrusted Data: Causes and Fixes

Turning untrusted bytes back into objects can run attacker-chosen code. How pickle, YAML, PHP and Java deserialization go wrong, and the fixes that hold up.

Safeguard Security Team
5 min read

CWE-502 is a product that deserializes untrusted data without making sure the resulting data is valid. It affects Java, Ruby, PHP, Python, JavaScript and, per MITRE, any technology that rebuilds objects from bytes.

Serialization turns an object into bytes for storage or transport. Deserialization turns the bytes back into an object. If the bytes come from an attacker, the attacker decides what object you get.

How it happens

Many serialization formats do more than carry data. They carry type information and sometimes behavior, so the reader creates objects of types the sender names, and runs code as part of building them. The program never asked for those objects, but it still builds them.

Python's pickle is the classic case. A pickled object can name a function to call when it is loaded.

# Vulnerable: loading bytes from a request
import pickle
def handler(request):
    obj = pickle.loads(request.body)   # attacker controls the bytes
    return obj.name

The record shows the same weakness across ecosystems:

  • CVE-2024-37052: insecure deserialization in a platform for managing AI and ML applications and models, allowing code execution through a crafted pickled object in a model file.
  • CVE-2024-37288: deserialization of untrusted YAML data in a dashboard for querying and visualizing Elasticsearch data.
  • CVE-2024-9314: PHP object injection in a WordPress plugin for AI based SEO.
  • CVE-2019-12799: a chain where a bypass of an untrusted deserialization fix was possible by using an assumed trusted class.

Note the model file example. Files you download and load are inputs too, not only network requests.

MITRE also describes gadgets: sequences of instances and method calls, often found in libraries, that can run themselves during deserialization. The attacker does not need a bug in your code. They chain together classes that already exist on your classpath.

What an attacker gains

MITRE lists three kinds of impact.

  • Integrity: attackers can modify objects or data assumed to be safe, change state without using accessor functions, or invoke unexpected functions.
  • Availability: resource consumption of CPU. If a function decides when to stop by looking for a marker in the data, a crafted input can make it never stop.
  • Other, varying by context: the effect depends on which objects and methods are deserialized and how they are used. Trusting the code inside a deserialized object is dangerous and can enable exploitation.

In the worst case this is remote code execution.

How to find it

Static analysis can find some instances by tracing data flow from untrusted input to a deserialization call. By hand, search for the calls and check where their input comes from:

  • Python: pickle.load, pickle.loads, yaml.load, shelve
  • Java: ObjectInputStream.readObject
  • PHP: unserialize
  • Ruby: Marshal.load, YAML.load

Then trace each one back. Is the input a request body, a cookie, a message queue item, a file from a user, or a downloaded model? If so, it is untrusted.

Check your dependencies as well. Gadget chains live in libraries, and many of the CVEs above are issues in packages you may use. The free in-browser scan checks a manifest of up to 100 dependencies without a login. Reachability analysis tells you whether your code calls the affected function, so you can separate real exposure from noise.

How to fix it

The best fix is to stop deserializing untrusted data with a format that can carry types. Use plain data formats such as JSON and read them into known structures. MITRE's mitigations add layers where that is not possible.

  1. Sign or seal the data. Use the signing features of the language, for example an HMAC, so tampered data is rejected before it is parsed. MITRE cautions that cryptography applied only on the client is still client side security and can be bypassed if the client is compromised.
  2. Populate a new object instead of deserializing straight into one. The data then passes through normal input validation.
  3. Explicitly define a final method that blocks deserialization for classes that should never be rebuilt.
  4. Mark fields transient so sensitive or environment based values are not carried over. After a round trip they come back empty.
  5. Limit gadgets. Remove unnecessary types and add only acceptable classes to an allowlist. MITRE's note: new gadgets are constantly found, so an allowlist alone is not sufficient.
  6. In operation, an application firewall can help when you cannot fix the code, for example in third party software. MITRE rates its effectiveness as moderate: it might not cover all input vectors and may block legitimate requests.
# Safer: a data-only format, read into a known shape
import json
def handler(request):
    data = json.loads(request.body)
    name = str(data.get("name", ""))[:100]
    return name

For YAML, use the safe loader (yaml.safe_load), which builds only basic types.

Try it yourself

Click through it at your own pace, or open it full screen.

Quick check

Why is a downloaded model file untrusted input?

Its contents come from outside your control, and some formats can run code when loaded. The pickle based CVE above is an example.

Is an allowlist of classes enough?

No. MITRE notes new gadgets are constantly being discovered, so use it with other controls, ideally by avoiding type carrying formats for untrusted data.

What does signing the data buy you?

You can reject tampered data before parsing it, as long as the signing key stays on the server side.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.