Insecure Deserialization Prevention Guide

Type: Software Reference Confidence: 0.95 Sources: 7 Verified: 2026-02-27 Freshness: 2026-02-27

TL;DR

Constraints

Quick Reference

Deserialization Threat/Fix Checklist by Language

#LanguageVulnerable APIRiskSafe AlternativeNotes
1JavaObjectInputStream.readObject()Critical -- RCE via gadget chains (ysoserial)JSON (Jackson/Gson), XML (JAXB), ProtobufOverride resolveClass() for allowlisting if unavoidable
2JavaXMLDecoder with user inputCritical -- arbitrary method invocationJAXB, StAX, DOM parsersNever use XMLDecoder with untrusted data
3JavaXStream < 1.4.17Critical -- RCE via type manipulationXStream >= 1.4.17 with allowlist, or JacksonSet XStream.allowTypes() explicitly
4Pythonpickle.loads() / pickle.load()Critical -- arbitrary code via __reduce__json.loads(), msgpack, protobufNo safe way to use pickle with untrusted data
5Pythonyaml.load() (PyYAML)Critical -- code execution via !!python/objectyaml.safe_load()PyYAML >= 6.0 defaults to safe_load
6PHPunserialize()Critical -- object injection via magic methodsjson_decode() / json_encode()POP chains exploit __wakeup, __destruct
7.NETBinaryFormatter.Deserialize()Critical -- RCE, cannot be securedSystem.Text.Json, DataContractSerializerRemoved entirely in .NET 9
8.NETTypeNameHandling != None (JSON.Net)High -- type confusion RCESet TypeNameHandling.NoneDefault is None; never change for untrusted data
9RubyMarshal.load()Critical -- arbitrary object instantiationJSON.parse(), Oj.safe_loadNo safe configuration exists
10Node.jsnode-serialize / serialize-to-jsCritical -- RCE via IIFE injectionJSON.parse() / JSON.stringify()Avoid any library that serializes functions
11Anyeval() on serialized stringsCritical -- direct code executionLanguage-specific safe parsersNever eval user input in any context

Decision Tree

START: Does your application deserialize data from untrusted sources?
├── NO → No insecure deserialization risk (verify trust boundaries)
├── YES
    ├── Can you switch to a data-only format (JSON, XML, Protobuf)?
    │   ├── YES → Replace native serialization with JSON/Protobuf (BEST option)
    │   └── NO
    │       ├── Java? → Override resolveClass() with allowlist + use ObjectInputFilter (Java 9+)
    │       ├── Python? → Replace pickle with json.loads(); use yaml.safe_load() for YAML
    │       ├── .NET? → Migrate to System.Text.Json; BinaryFormatter removed in .NET 9
    │       ├── PHP? → Replace unserialize() with json_decode()
    │       ├── Ruby? → Replace Marshal.load with JSON.parse; use Oj.safe_load
    │       └── Node.js? → Use JSON.parse/JSON.stringify only; remove node-serialize
    └── Add defense-in-depth: HMAC signing, integrity checks, monitoring

Step-by-Step Guide

1. Audit your codebase for deserialization sinks

Identify every location where untrusted data is deserialized. Use static analysis or grep for known dangerous patterns. [src2]

# Java: find ObjectInputStream usage
grep -rn 'ObjectInputStream\|readObject\|XMLDecoder\|XStream' --include="*.java" .

# Python: find pickle and unsafe YAML
grep -rn 'pickle\.load\|pickle\.loads\|yaml\.load\|jsonpickle' --include="*.py" .

# PHP: find unserialize calls
grep -rn 'unserialize(' --include="*.php" .

# .NET: find BinaryFormatter and unsafe TypeNameHandling
grep -rn 'BinaryFormatter\|TypeNameHandling' --include="*.cs" .

# Node.js: find node-serialize and eval-based deserialization
grep -rn "node-serialize\|serialize-to-js\|eval(" --include="*.js" .

Verify: Count all findings -- each one is a potential RCE vulnerability.

2. Replace native serialization with JSON

Switch from native object serialization to data-only formats. JSON cannot represent executable code, eliminating the entire attack class. [src1]

# Python: BEFORE (vulnerable)
import pickle
data = pickle.loads(user_input)  # RCE vulnerability

# Python: AFTER (safe)
import json
data = json.loads(user_input)  # Data only, no code execution

Verify: Run audit commands again -- dangerous patterns should be eliminated.

3. Apply allowlist filtering when native deserialization is unavoidable

Restrict which classes can be deserialized using built-in filtering mechanisms. [src1]

// Java 9+: ObjectInputFilter (built-in)
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.myapp.dto.*;!*"  // Allow only com.myapp.dto, deny all else
);
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(filter);

Verify: Attempt to deserialize a non-allowlisted class -- should throw InvalidClassException.

4. Sign serialized data with HMAC

When you must serialize data that will be stored or transmitted, sign it to detect tampering. [src3]

import hmac, hashlib, json
SECRET_KEY = b'your-secret-key-from-env'

def serialize_signed(data: dict) -> str:
    payload = json.dumps(data, sort_keys=True)
    sig = hmac.new(SECRET_KEY, payload.encode(), hashlib.sha256).hexdigest()
    return f"{payload}|{sig}"

def deserialize_verified(signed_data: str) -> dict:
    payload, sig = signed_data.rsplit('|', 1)
    expected = hmac.new(SECRET_KEY, payload.encode(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected):
        raise ValueError("Tampered data detected")
    return json.loads(payload)

Verify: Modify any byte in the signed payload -- should raise ValueError.

5. Monitor and log deserialization events

Add observability to detect exploitation attempts in production. [src2]

ObjectInputFilter loggingFilter = filterInfo -> {
    Class<?> clazz = filterInfo.serialClass();
    if (clazz != null) {
        logger.info("Deserialization attempt: {}", clazz.getName());
        if (!ALLOWED_CLASSES.contains(clazz.getName())) {
            logger.warn("BLOCKED: {}", clazz.getName());
            return ObjectInputFilter.Status.REJECTED;
        }
    }
    return ObjectInputFilter.Status.ALLOWED;
};

Verify: Check logs for BLOCKED entries after deploying the filter.

Code Examples

Python: Safe Data Exchange with JSON

# Input:  Untrusted user data (API request body, file upload)
# Output: Validated Python dict

import json
from dataclasses import dataclass

def deserialize_user_data(raw: str) -> dict:
    try:
        data = json.loads(raw)  # Safe -- data only
    except json.JSONDecodeError:
        raise ValueError("Invalid JSON input")
    if not isinstance(data, dict):
        raise ValueError("Expected JSON object")
    return data

# UNSAFE -- NEVER use with untrusted data:
# pickle.loads(raw)       -- arbitrary code execution
# yaml.load(raw)          -- code exec via !!python/object
# jsonpickle.decode(raw)  -- code exec via py/object

Java: Jackson with Strict Type Binding

// Input:  Untrusted JSON string from HTTP request
// Output: Validated DTO object

import com.fasterxml.jackson.databind.ObjectMapper;  // 2.15+
import com.fasterxml.jackson.databind.DeserializationFeature;

public class SafeDeserializer {
    private static final ObjectMapper mapper = new ObjectMapper()
        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        // CRITICAL: never enable default typing
        // mapper.enableDefaultTyping() <-- NEVER DO THIS

    public static <T> T deserialize(String json, Class<T> type) {
        try {
            return mapper.readValue(json, type);
        } catch (Exception e) {
            throw new IllegalArgumentException("Invalid input", e);
        }
    }
}

Node.js: Safe JSON Parsing with Prototype Pollution Protection

// Input:  Untrusted string from HTTP request body
// Output: Validated JavaScript object

function safeDeserialize(raw) {
  let parsed;
  try {
    parsed = JSON.parse(raw);
  } catch (e) {
    throw new Error('Invalid JSON input');
  }
  // Protect against prototype pollution
  if (parsed.__proto__ || parsed.constructor) {
    delete parsed.__proto__;
    delete parsed.constructor;
  }
  return parsed;
}

// UNSAFE -- NEVER use with untrusted data:
// require('node-serialize').unserialize(raw)  -- IIFE RCE
// eval('(' + raw + ')')                       -- direct RCE

Anti-Patterns

Wrong: Using pickle to deserialize user-uploaded files

# BAD -- pickle executes arbitrary code during deserialization
import pickle

def load_user_config(uploaded_file):
    return pickle.load(uploaded_file)
# Attacker crafts payload with __reduce__ running os.system("rm -rf /")

Correct: Use JSON for user data exchange

# GOOD -- JSON is data-only, no code execution possible
import json

def load_user_config(uploaded_file):
    return json.load(uploaded_file)

Wrong: Java ObjectInputStream without class filtering

// BAD -- any class on the classpath can be instantiated
ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
Command cmd = (Command) ois.readObject();
// ysoserial gadget chains execute before the cast is checked

Correct: Java ObjectInputFilter with strict allowlist

// GOOD -- only explicitly allowed classes can deserialize
ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
ois.setObjectInputFilter(ObjectInputFilter.Config.createFilter(
    "com.myapp.dto.Command;!*"
));
Command cmd = (Command) ois.readObject();

Wrong: PHP unserialize on cookie data

// BAD -- user controls cookie content, enabling object injection
$preferences = unserialize($_COOKIE['prefs']);
// Attacker sets cookie to exploit __wakeup() or __destruct()

Correct: PHP json_decode for cookie data

// GOOD -- JSON cannot instantiate PHP objects
$preferences = json_decode($_COOKIE['prefs'], true);
if ($preferences === null && json_last_error() !== JSON_ERROR_NONE) {
    $preferences = [];
}

Wrong: .NET BinaryFormatter for session state

// BAD -- BinaryFormatter is inherently unsafe, removed in .NET 9
BinaryFormatter formatter = new BinaryFormatter();
object session = formatter.Deserialize(stream);
// Microsoft: "equivalent to launching an executable"

Correct: .NET System.Text.Json for data exchange

// GOOD -- System.Text.Json deserializes to known types only
using System.Text.Json;
var session = JsonSerializer.Deserialize<SessionData>(stream);

Wrong: Ruby Marshal.load on user input

# BAD -- Marshal can instantiate any Ruby class
data = Marshal.load(Base64.decode64(params[:data]))
# Universal RCE gadget chain exists for Ruby 2.x+

Correct: Ruby JSON.parse for user data

# GOOD -- JSON produces only primitive types
require 'json'
data = JSON.parse(params[:data])

Common Pitfalls

Diagnostic Commands

# Scan Java project for deserialization sinks
grep -rn 'ObjectInputStream\|readObject\|XMLDecoder\|XStream' --include="*.java" .

# Scan Python project for pickle and unsafe YAML
grep -rn 'pickle\.\|yaml\.load\|yaml\.unsafe_load\|jsonpickle' --include="*.py" .

# Scan PHP project for unserialize
grep -rn 'unserialize(' --include="*.php" .

# Scan .NET project for BinaryFormatter
grep -rn 'BinaryFormatter\|TypeNameHandling' --include="*.cs" .

# Scan Node.js project for dangerous patterns
grep -rn "node-serialize\|serialize-to-js\|eval(" --include="*.js" .

# Check Java dependencies for known gadget libraries
mvn dependency:tree | grep -E 'commons-collections|spring-beans|groovy'

# Run npm audit for deserialization vulnerabilities
npm audit 2>/dev/null | grep -i "deserializ\|prototype pollution"

# Test with ysoserial (Java -- penetration testing only)
java -jar ysoserial.jar CommonsCollections1 'id' | base64

Version History & Compatibility

Standard/ToolVersionStatusKey Feature
OWASP Top 102021CurrentA08: Software and Data Integrity Failures
OWASP Top 102017PreviousA8: Insecure Deserialization (dedicated category)
CWE-5024.19CurrentDeserialization of Untrusted Data
.NET BinaryFormatter.NET 9RemovedThrows PlatformNotSupportedException
.NET BinaryFormatter.NET 7-8ObsoleteWarning on use; opt-in still available
Java ObjectInputFilterJava 9+CurrentBuilt-in class allowlist/denylist
Java Module SystemJava 17+CurrentRestricts reflection-based gadget chains
PyYAML6.0+CurrentDefaults to safe_load behavior
XStream1.4.17+CurrentBuilt-in allowlist via allowTypes()
Jackson2.15+CurrentPolymorphic typing disabled by default

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Application accepts serialized objects from users (cookies, forms, APIs)All data exchange uses JSON/XML/Protobuf exclusivelyStandard input validation
Legacy system uses Java ObjectInputStream for RPCBuilding a new service with modern frameworksgRPC + Protobuf, REST + JSON
Session state stored in native serialization formatSessions use signed JWTs or encrypted cookiesJWT/session library with JSON serializer
ML models are loaded from untrusted sources (pickle files)Models from your own pipeline with integrity verificationSafeTensors, ONNX format

Important Caveats

Related Units