Skip to content

4.37 Review Insecure Deserialization

4.37 - Review Insecure Deserialization

Insecure deserialization appears when applications rebuild objects from untrusted byte streams or structured payloads without validation. Review Java ObjectInputStream, Python pickle, .NET BinaryFormatter, and YAML loaders that construct arbitrary types. Trace serialized data from cookies, caches, message queues, and file uploads to deserialization sinks.

What This Vulnerability Is

Deserialization converts stored or transmitted data back into runtime objects. When the format allows arbitrary types—or when gadget chains exist in the classpath—attackers can craft payloads that execute code during deserialization. Native object serialization in Java and pickle in Python are high-risk; even JSON can be unsafe when polymorphic type metadata is honored blindly.

The unsafe assumption is that serialized data is trusted because it came from an internal service or was signed without verification. Attackers may tamper with cookies, replay messages, or upload malicious files. Mitigation starts with avoiding deserialization of user-controlled input, using safer formats, and keeping dependencies patched. This pattern maps to CWE-502 (Deserialization of Untrusted Data).

Vulnerability Characteristics (Where to Identify Them)

Signal Where to look
Feature type Session restore, cache reload, import/export, inter-service messaging, plugin state
Native serialization ObjectInputStream, pickle.loads, PHP unserialize, .NET BinaryFormatter
Polymorphic parsers Jackson default typing, XStream, XMLDecoder without type allowlists
Data sources HTTP cookies, hidden fields, Redis, Kafka, session stores, uploaded files
Encrypted blobs Serialization wrapped in encryption without authentication or integrity checks
Dependency risk commons-collections and similar gadget-bearing libraries on the classpath
Safer alternatives missing JSON/protobuf into plain DTOs with schema validation not used

Attack Payloads

Use crafted payloads only in isolated lab environments with authorization. Binary gadget chains vary by classpath and library versions.

Pattern 1: Python pickle opcode injection

Pickle opcodes can invoke os.system, eval, or arbitrary callables during pickle.loads. Tools such as pickle-assemble generate test blobs—never paste untrusted pickle bytes into production parsers.

Pattern 2: Java ysoserial gadget chains

# Common gadget-bearing libraries on classpath:
# commons-collections, commons-beanutils, spring, groovy
# Generate with ysoserial for authorized pentest:
java -jar ysoserial.jar CommonsCollections6 'id' | base64

Pattern 3: Jackson default typing

["com.example.Evil", {"cmd": "id"}]
{"@class": "java.lang.ProcessBuilder", "command": ["id"]}

Pattern 4: YAML type tags (Python)

!!python/object/apply:os.system ["id"]
!!python/object/new:subprocess.check_output [["id"]]

Pattern 5: .NET TypeNameHandling

{
  "$type": "System.Windows.Data.ObjectDataProvider, PresentationFramework",
  "MethodName": "Start",
  "ObjectInstance": { "$type": "System.Diagnostics.Process, System" }
}

Pattern 6: Ruby Marshal load from cache

# Redis session blob — attacker replaces value with crafted Marshal stream
session = Marshal.load(redis.get("sess:#{sid}"))

Pattern 7: PHP phar deserialization

// phar:// wrapper triggers metadata deserialization on file_exists()
file_exists('phar://uploads/evil.phar/b.txt');

Language-Specific Sinks and Dangerous APIs

Python

pickle.loads(data)
pickle.load(file)
yaml.load(data)  # Loader=yaml.Loader or unsafe default
yaml.unsafe_load(data)
marshal.loads(data)
shelve.open(user_path)
jsonpickle.decode(data)

Also review: torch.load, numpy.load(..., allow_pickle=True), dill.loads, Redis/cache storing pickled objects.

Java

ObjectInputStream.readObject()
ObjectInputStream.readUnshared()
XMLDecoder.readObject()
XStream.fromXML(userXml);
new ObjectMapper().enableDefaultTyping(...);
JSON.parseObject(json, Object.class);  // Fastjson autoType
Serializable.readObject in RMI/JMX endpoints

MyBatis, Hibernate, and Spring remoting with Java serialization on the wire.

C

BinaryFormatter.Deserialize(stream);
SoapFormatter.Deserialize(stream);
LosFormatter.Deserialize(...);
JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings {
    TypeNameHandling = TypeNameHandling.All
});
DataContractSerializer with known types expanded from user input

JavaScript

node-serialize.unserialize(userInput);
// eval(JSON.parse) patterns that revive functions
require('serialize-javascript') with untrusted revive

Go

gob.NewDecoder(r).Decode(&v)  // from untrusted client
encoding/gob on network input without schema
json.Unmarshal into map[string]interface{} then type assertions on @type fields

PHP

unserialize($_COOKIE['session']);
unserialize(file_get_contents('php://input'));

Sample Vulnerable Code in Python

import pickle
import yaml
from flask import Flask, request

app = Flask(__name__)

@app.route("/import", methods=["POST"])
def import_state():
    # Attacker-controlled bytes execute arbitrary code during unpickling
    state = pickle.loads(request.get_data())
    return process(state)

@app.route("/config", methods=["POST"])
def load_config():
    # yaml.load without SafeLoader may construct arbitrary Python objects
    config = yaml.load(request.get_data(), Loader=yaml.Loader)
    apply_config(config)

def restore_session(cookie_value: bytes):
    return pickle.loads(cookie_value)

Step-by-Step Review Walkthrough

  1. Search deserialization APIs. Look for ObjectInputStream, readObject, pickle.loads, yaml.load, BinaryFormatter, and unserialize.
  2. Identify data sources. Trace cookies, form fields, caches, message queues, session stores, and uploaded files to deserialization sinks.
  3. Check type metadata influence. Review JSON @type fields, YAML tags, and XML entities that select arbitrary classes.
  4. Review classpath gadgets. Outdated commons-collections and similar JARs raise exploitability even when input is partially trusted.
  5. Inspect encrypted blobs. Encryption without authentication does not prevent tampering of serialized payloads.
  6. Confirm safer alternatives. Prefer JSON or protobuf into plain DTOs with schema validation over object graphs.
  7. Verify dependency scanning. Dependabot, OSV, or Snyk should cover libraries involved in deserialization paths.

Risk Impact Analysis

Remote code execution. Gadget chains in Java and pickle opcodes in Python often achieve full process compromise from a single malicious blob.

Authentication bypass. Tampered session objects may elevate privileges or impersonate users when deserialized into application state.

Lateral movement. Message queue and cache deserialization flaws let attackers pivot through internal services.

Data integrity loss. Attackers may alter business objects in transit when integrity checks are absent.

Difficult detection. Deserialization exploits may leave few obvious log signatures compared to SQL injection or XSS.

Vulnerable Examples in Other Languages

Java

public User loadSession(byte[] blob) throws Exception {
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(blob));
    return (User) ois.readObject(); // attacker-controlled bytes
}

public Object importState(InputStream body) throws Exception {
    ObjectInputStream ois = new ObjectInputStream(body);
    return ois.readObject();
}

// Jackson default typing on untrusted JSON
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
User user = mapper.readValue(jsonFromClient, User.class);

C

public object LoadCache(string base64)
{
    var bytes = Convert.FromBase64String(base64);
    var formatter = new BinaryFormatter();
    using var ms = new MemoryStream(bytes);
    return formatter.Deserialize(ms);
}

public T DeserializeJson<T>(string json)
{
    return JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings
    {
        TypeNameHandling = TypeNameHandling.All // polymorphic gadget risk
    });
}

Go

// Accepting gob from clients without schema validation
func decodeProfile(r io.Reader) (*Profile, error) {
    dec := gob.NewDecoder(r)
    var p Profile
    return &p, dec.Decode(&p)
}

func restoreSession(cookie string) (map[string]interface{}, error) {
    data, _ := base64.StdEncoding.DecodeString(cookie)
    var state map[string]interface{}
    return state, json.Unmarshal(data, &state) // no signature or type allowlist
}

Fix: Safer Patterns and Libraries to Use

Python

Parse JSON with schema validation. Never unpickle untrusted bytes.

import json
from pydantic import BaseModel, ValidationError

class ImportState(BaseModel):
    version: int
    items: list[str]

@app.route("/import", methods=["POST"])
def import_state():
    try:
        data = json.loads(request.get_data())
        state = ImportState.model_validate(data)
    except (json.JSONDecodeError, ValidationError):
        abort(400)
    return process(state)

@app.route("/config", methods=["POST"])
def load_config():
    config = yaml.safe_load(request.get_data())
    apply_config(config)

Store session state server-side with opaque IDs instead of pickled cookies. See yaml.safe_load and pydantic.

Java

Map JSON to explicit DTOs. Disable default typing. Use ObjectInputFilter when legacy serialization cannot be removed.

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;

public User parseUser(String jsonInput) {
    Gson gson = new Gson();
    try {
        return gson.fromJson(jsonInput, User.class);
    } catch (JsonSyntaxException e) {
        throw new IllegalArgumentException("Invalid JSON input");
    }
}
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.example.User;!*");
ois.setObjectInputFilter(filter);

See JEP 290: Filter Incoming Serialization Data and Gson user guide.

C

Deserialize into known types with System.Text.Json. Avoid BinaryFormatter.

public ImportState LoadState(string json)
{
    return JsonSerializer.Deserialize<ImportState>(json, new JsonSerializerOptions
    {
        PropertyNameCaseInsensitive = false,
        AllowTrailingCommas = false
    }) ?? throw new JsonException("Invalid payload");
}

In Newtonsoft.Json, set TypeNameHandling = TypeNameHandling.None on external input. See System.Text.Json overview.

Go

Unmarshal JSON into structs with unknown field rejection and size limits.

func decodeProfile(r io.Reader) (*Profile, error) {
    dec := json.NewDecoder(io.LimitReader(r, 1<<20))
    dec.DisallowUnknownFields()
    var p Profile
    if err := dec.Decode(&p); err != nil {
        return nil, err
    }
    return &p, nil
}

Prefer protobuf or msgpack with explicit message types instead of gob from clients.

Verify During Review

  • User-controlled input is not passed to native object deserialization APIs.
  • JSON, XML, and YAML parsers use strict schemas, safe loaders, and disabled polymorphic type gadgets.
  • Session and cache blobs use signed and authenticated formats, or store opaque server-side keys instead of serialized objects.
  • Dependencies with known deserialization CVEs are patched or removed.
  • Safer data formats (JSON with DTOs) replace Java serialization and pickle in cross-trust-boundary flows.
  • Error handling does not echo serialized payload details that aid exploit crafting.

Reference