Overview
Insecure Deserialization occurs when an application deserializes data from an untrusted source without proper validation. Deserialization is the process of converting a byte stream or structured text (like XML/YAML) back into a live object in memory. If an attacker can control this serialized data, they can craft a malicious object that, when instantiated, can execute arbitrary code, bypass logic, or cause a denial of service.Business Impact
This is often one of the most critical vulnerabilities, frequently leading directly to Remote Code Execution (RCE) on the application server. Exploitation involves “gadget chains”—leveraging pieces of existing, legitimate code in the application in unexpected ways to perform malicious actions during the deserialization process.Reference Details
CWE ID: CWE-502
OWASP Top 10 (2021): A08:2021 - Software and Data Integrity Failures
Severity: Critical
Framework-Specific Analysis and Remediation
The universal and most effective mitigation is to never deserialize data from untrusted sources using native, object-oriented serialization formats. Instead, use safe, data-only formats like JSON for all data interchange. If a native format is absolutely required, use features that restrict which classes can be instantiated or use a digital signature to verify the integrity and authenticity of the serialized data before processing.- Python
- Java
- .NET(C#)
- PHP
- Node.js
- Ruby
Framework Context
Python’spickle module is the primary mechanism for native object serialization, and it is notoriously insecure. The official documentation explicitly warns against unpickling data from untrusted sources. PyYAML’s load() function is also unsafe.Vulnerable Scenario 1: Unpickling a Session Cookie
A web application stores a user’s session object as a pickled, base64-encoded string in a cookie.Copy
# middleware/session.py
import pickle
import base64
class PickleSessionMiddleware:
def process_request(self, request):
session_data = request.COOKIES.get('session')
if session_data:
# DANGEROUS: An attacker can replace their cookie with a malicious
# pickled object that executes code upon deserialization.
request.session = pickle.loads(base64.b64decode(session_data))
Vulnerable Scenario 2: Processing Data from a Task Queue
A Celery worker receives a task whose arguments include a YAML-serialized object.Copy
# tasks.py
import yaml
@shared_task
def process_report(report_data_yaml):
# DANGEROUS: The default yaml.load() can construct any Python object
# and even execute arbitrary functions.
report = yaml.load(report_data_yaml, Loader=yaml.Loader)
# ... process report ...
Mitigation and Best Practices
Never usepickle or yaml.load() for data that has passed through an untrusted environment. Use json for all data interchange. For YAML, always use yaml.safe_load(). Django’s built-in session framework is secure and uses a signed JSON-based backend by default; rely on it instead of rolling your own.Secure Code Example
Copy
# middleware/session.py (Secure Version)
import json
from django.core.signing import Signer, BadSignature
class JsonSessionMiddleware:
def process_request(self, request):
session_data = request.COOKIES.get('session')
signer = Signer()
if session_data:
try:
# SAFE: 1. Verify the signature to prevent tampering.
# 2. Use json.loads(), which only creates simple data types.
unsigned_data = signer.unsign(session_data)
request.session = json.loads(unsigned_data)
except (BadSignature, json.JSONDecodeError):
request.session = {} # Handle tampered/invalid data
Testing Strategy
Testing for this is complex. It involves creating a known RCE payload forpickle (using a tool like ysoserial.net) and submitting it to the vulnerable endpoint. The test would then check for the side-effect of the code execution (e.g., a file being created on the server, or a network callback).Copy
# A conceptual test
def test_session_deserialization_rce(self):
# 1. Generate a malicious pickle payload that creates a file '/tmp/pwned'
malicious_payload = generate_pickle_rce_payload("touch /tmp/pwned")
encoded_payload = base64.b64encode(malicious_payload).decode()
# 2. Set the cookie and make a request
self.client.cookies['session'] = encoded_payload
self.client.get('/')
# 3. Assert that the file was NOT created
self.assertFalse(os.path.exists('/tmp/pwned'))
Framework Context
Java’s nativeObjectInputStream is the classic vector for insecure deserialization. Spring Boot applications should avoid it at all costs and use a safe library like Jackson for JSON processing.Vulnerable Scenario 1: Reading from an HttpInvokerServiceExporter
This is a legacy Spring feature that uses Java Serialization to handle remote procedure calls over HTTP.Copy
// config/RemotingConfig.java
@Bean(name = "/userService")
public HttpInvokerServiceExporter userService(UserService userService) {
// DANGEROUS: This endpoint will deserialize any Java object sent to it,
// making it a prime target for RCE gadget chain exploits.
HttpInvokerServiceExporter exporter = new HttpInvokerServiceExporter();
exporter.setService(userService);
exporter.setServiceInterface(UserService.class);
return exporter;
}
Vulnerable Scenario 2: Deserializing an Object from a File Upload
An admin function allows uploading a file containing a serializedConfiguration object.Copy
// controller/AdminController.java
@PostMapping("/upload-config")
public void uploadConfig(@RequestParam("file") MultipartFile file) throws Exception {
try (InputStream is = file.getInputStream();
ObjectInputStream ois = new ObjectInputStream(is)) {
// DANGEROUS: Deserializes the uploaded file content directly.
Configuration config = (Configuration) ois.readObject();
// ... apply config ...
}
}
Mitigation and Best Practices
Do not use Java Serialization. Use REST endpoints with JSON for communication and a library like Jackson for safe data binding. If native serialization is unavoidable, implement a look-ahead deserialization agent or use a library likeValidatingObjectInputStream to strictly allow-list the classes that are permitted to be deserialized.Secure Code Example
Copy
// controller/AdminController.java (Secure JSON Version)
import com.fasterxml.jackson.databind.ObjectMapper;
@PostMapping("/upload-config")
public void uploadConfig(@RequestParam("file") MultipartFile file) throws IOException {
// SAFE: Jackson is used to deserialize JSON into a specific, known DTO class.
// It does not execute arbitrary code from the input data.
ObjectMapper mapper = new ObjectMapper();
try {
ConfigurationDTO config = mapper.readValue(file.getInputStream(), ConfigurationDTO.class);
// ... apply config from the safe DTO ...
} catch (JsonProcessingException e) {
// Handle invalid JSON
}
}
Testing Strategy
Use a tool likeysoserial to generate a Java deserialization payload for a common gadget chain (e.g., CommonsCollections1). Send this payload to the vulnerable endpoint. A successful exploit would execute the payload (e.g., run calc.exe or touch /tmp/pwned). A test should assert that this side-effect does not occur.Copy
// A conceptual test
@Test
void uploadConfig_withMaliciousPayload_shouldNotExecuteCode() {
// 1. Generate ysoserial payload that creates a file.
byte[] payload = generateYsoserialPayload();
MockMultipartFile file = new MockMultipartFile("file", "config.ser", "application/octet-stream", payload);
// 2. Call the vulnerable endpoint
mockMvc.perform(multipart("/upload-config").file(file));
// 3. Assert that the side-effect (file creation) did not happen.
File pwnedFile = new File("pwned");
assertFalse(pwnedFile.exists());
}
Framework Context
TheBinaryFormatter is .NET’s equivalent of Java Serialization and is extremely dangerous. Microsoft has officially marked it as obsolete and insecure. The recommended alternative is a data-only format like JSON, processed with libraries like System.Text.Json or Newtonsoft.Json.Vulnerable Scenario 1: Using BinaryFormatter in Session State
An older application might be configured to use BinaryFormatter to serialize session objects.Copy
// An example of state being read from a request
public UserProfile GetUserProfileFromState(string state)
{
byte[] decodedState = Convert.FromBase64String(state);
using var stream = new MemoryStream(decodedState);
// DANGEROUS: BinaryFormatter is insecure and can lead to RCE.
var formatter = new BinaryFormatter();
return (UserProfile)formatter.Deserialize(stream);
}
Vulnerable Scenario 2: Json.NET with Insecure TypeNameHandling
While Json.NET is generally safe, configuring it with TypeNameHandling.All allows the JSON data to specify which .NET type to create, reintroducing deserialization vulnerabilities.Copy
// Controllers/ApiController.cs
[HttpPost]
public IActionResult Post([FromBody] JObject jsonData)
{
var settings = new JsonSerializerSettings
{
// DANGEROUS: This setting allows an attacker to control which object gets created.
TypeNameHandling = TypeNameHandling.All
};
// The attacker can craft a JSON payload that instantiates an unexpected object type.
var obj = JsonConvert.DeserializeObject(jsonData.ToString(), settings);
// ...
}
Mitigation and Best Practices
Never useBinaryFormatter. Use a modern JSON serializer like System.Text.Json or Newtonsoft.Json with its default, safe settings (TypeNameHandling.None). Always deserialize into a known, expected type.Secure Code Example
Copy
// A secure way to handle user state
using System.Text.Json;
public UserProfile GetUserProfileFromState(string state)
{
try
{
// SAFE: System.Text.Json is a modern, secure serializer.
// It does not have the same RCE vulnerabilities as BinaryFormatter.
return JsonSerializer.Deserialize<UserProfile>(state);
}
catch (JsonException)
{
// Handle invalid data
return null;
}
}
Testing Strategy
Similar to Java, use a tool likeysoserial.net to generate a payload for a known .NET gadget chain. Submit this payload to the vulnerable endpoint and verify that the code execution does not occur.Copy
// Conceptual test for a vulnerable endpoint
[Fact]
public void StateDeserialization_WithGadgetPayload_ShouldNotCauseRCE()
{
// 1. Generate a malicious payload using ysoserial.net that writes a file.
string payload = GenerateNetRcePayload();
// 2. Call the vulnerable method
// ...
// 3. Assert that the malicious side-effect did not occur.
Assert.False(File.Exists("C:\\pwned.txt"));
}
Framework Context
PHP’s nativeunserialize() function is extremely dangerous with untrusted input because it can trigger “magic methods” (like __wakeup or __destruct) on any class available in the application’s scope, leading to Property-Oriented Programming (POP) gadget chain exploits.Vulnerable Scenario 1: Processing a Signed URL with Serialized Data
Laravel’s signed URLs are great for security, but if the developer puts serialized user data inside them, they can still be exploited.Copy
// routes/web.php
Route::get('/process-report', function (Request $request) {
$reportParams = unserialize(base64_decode($request->input('params')));
// DANGEROUS: Even if the URL is signed, the 'params' data is from the user
// and could have been crafted before the URL was signed.
// The unserialize call is the vulnerability.
})->name('reports.process')->middleware('signed');
Vulnerable Scenario 2: Reading from a Queue
A queued job processes data that was originally serialized by another, potentially untrusted, part of the system.Copy
// app/Jobs/ProcessUserData.php
class ProcessUserData implements ShouldQueue
{
protected $userData;
public function __construct($userData) { $this->userData = $userData; }
public function handle()
{
// DANGEROUS: The job is unserialized by the Laravel worker. If an attacker
// can inject a malicious job object into the queue (e.g., Redis),
// it will be executed.
$user = unserialize($this->userData);
}
}
Mitigation and Best Practices
Never useunserialize() on any data that a user could have influenced. Use json_decode() for all data interchange. If you absolutely must use unserialize (which is highly discouraged), use the allowed_classes option (PHP 7+) to restrict which objects can be created.Secure Code Example
Copy
// routes/web.php (Secure Version)
Route::get('/process-report', function (Request $request) {
// SAFE: Use JSON, which is a data-only format and cannot instantiate arbitrary classes.
$reportParams = json_decode(base64_decode($request->input('params')), true);
if (json_last_error() !== JSON_ERROR_NONE) {
abort(400, 'Invalid parameters.');
}
// ... process the safe array ...
})->name('reports.process')->middleware('signed');
Testing Strategy
Use a tool like PHPGGC to generate a serialized PHP payload with a known gadget chain. Submit this payload to the vulnerable endpoint. The test should verify that the application returns an error and that the side-effect of the gadget chain (e.g., writing a file) did not occur.Copy
// tests/Feature/ReportProcessingTest.php
public function test_report_endpoint_is_not_vulnerable_to_deserialization()
{
// 1. Use a tool to generate a payload for a known gadget chain
// (e.g., from a library like Monolog or Guzzle used by Laravel)
$payload = '...malicious serialized string...';
$encodedPayload = base64_encode($payload);
$url = URL::temporarySignedRoute(
'reports.process', now()->addMinutes(5), ['params' => $encodedPayload]
);
$response = $this->get($url);
// A secure implementation using json_decode will fail.
$response->assertStatus(400);
// Also check that the side-effect did not happen.
$this->assertFileDoesNotExist(public_path('pwned.php'));
}
Framework Context
Standard Node.js (withJSON.parse) is inherently safe from this class of vulnerability because JSON is a data-only format. The risk comes from using insecure third-party libraries that attempt to serialize more complex data, including functions, like node-serialize.Vulnerable Scenario 1: Using the node-serialize Library
A web application uses the node-serialize library to store user preferences in a cookie.Copy
// app.js
const serialize = require('node-serialize');
app.get('/profile', (req, res) => {
if (req.cookies.profile) {
try {
// DANGEROUS: `unserialize()` can execute code. An attacker can craft
// a payload that runs a function on the server.
// For example: {"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('touch /tmp/pwned', function(e,s,st){});}()"}
var profile = serialize.unserialize(Buffer.from(req.cookies.profile, 'base64').toString());
} catch (e) {
// ...
}
}
});
Vulnerable Scenario 2: Using js-yaml in an Unsafe Mode
The js-yaml library, when used with its full (non-safe) load function, can execute JavaScript functions embedded in a YAML document.Copy
// routes/config.js
const yaml = require('js-yaml');
router.post('/apply', (req, res) => {
// DANGEROUS: The default `load()` is unsafe.
// An attacker can submit YAML with a `!!js/function` tag to run code.
const config = yaml.load(req.body.configData);
});
Mitigation and Best Practices
Stick to the native, safeJSON.parse() for all data serialization and deserialization needs. If using other format libraries like js-yaml, always use their “safe” loading methods (e.g., yaml.safeLoad()). Avoid any library that claims to serialize functions or other complex types.Secure Code Example
Copy
// app.js (Secure Version)
app.get('/profile', (req, res) => {
if (req.cookies.profile) {
try {
// SAFE: JSON.parse() only creates plain JavaScript objects and data types.
// It cannot execute functions or create arbitrary classes.
const profile = JSON.parse(Buffer.from(req.cookies.profile, 'base64').toString());
// ... process safe profile object ...
} catch (e) {
// Handle invalid JSON or tampered cookie
}
}
});
Testing Strategy
Write a test that base64-encodes a known malicious payload fornode-serialize and sends it in the cookie. The test should then check for the side-effect of the payload (e.g., a file being created) and assert that it did not happen.Copy
// tests/profile.test.js
const fs = require('fs');
it('should not be vulnerable to deserialization RCE', async () => {
// A known RCE payload for node-serialize that creates a file
const payload = '{"rce":"_$$ND_FUNC$$_function(){require(\'fs\').writeFileSync(\'pwned.txt\', \'\');}()"}';
const encodedPayload = Buffer.from(payload).toString('base64');
// Make the request with the malicious cookie
await request(app).get('/profile').set('Cookie', `profile=${encodedPayload}`);
// Assert that the malicious file was NOT created.
const fileExists = fs.existsSync('pwned.txt');
expect(fileExists).toBe(false);
});
Framework Context
Ruby’sMarshal.load is the equivalent of pickle and is highly insecure with untrusted data. Rails uses it for its default cookie-based session store, but crucially, it signs the data with a secret key to prevent tampering. The vulnerability appears if a developer uses Marshal.load manually on data they think is safe.Vulnerable Scenario 1: Manual Deserialization of a Parameter
An endpoint accepts a Base64-encoded, marshaled Ruby object from a trusted client, but an attacker can hit the endpoint directly.Copy
# app/controllers/api/v1/data_controller.rb
class Api::V1::DataController < ApplicationController
def create
encoded_object = params[:data]
# DANGEROUS: The data is coming from an external source and is being
# deserialized directly. An attacker can craft a gadget chain payload.
object = Marshal.load(Base64.decode64(encoded_object))
# ...
end
end
Vulnerable Scenario 2: Insecure Use of YAML.load
Similar to other languages, Ruby’s standard YAML library has a load method that is unsafe and can instantiate arbitrary objects.Copy
# app/services/config_importer.rb
require 'yaml'
class ConfigImporter
def import(yaml_data)
# DANGEROUS: YAML.load is unsafe. An attacker could provide a YAML
# document that creates and executes a malicious object.
config = YAML.load(yaml_data)
end
end
Mitigation and Best Practices
Never useMarshal.load or YAML.load on data that has ever been in the hands of a user. Use JSON.parse for data interchange. If you need to pass complex objects between your own trusted services, ensure the data is signed with a strong MAC (like Rails does for its sessions) to verify its integrity and authenticity before deserializing.Secure Code Example
Copy
# app/controllers/api/v1/data_controller.rb (Secure Version)
class Api::V1::DataController < ApplicationController
def create
json_data = params[:data]
begin
# SAFE: JSON.parse only creates basic Ruby hashes, arrays, and primitives.
# It cannot be used to trigger gadget chain exploits.
hash = JSON.parse(json_data)
# ... process the safe hash ...
rescue JSON::ParserError
render json: { error: 'Invalid data format' }, status: :bad_request
end
end
end
Testing Strategy
Craft a test that generates a marshaled object payload containing a known gadget chain. Send this to the vulnerable endpoint and assert that the application does not execute the malicious code. This often involves checking that a side-effect (like a file being created or a command being run) did not occur.Copy
# spec/requests/data_api_spec.rb
RSpec.describe "Data API", type: :request do
it "is not vulnerable to remote code execution via Marshal.load" do
# This payload is a simplified example of a gadget chain
# In a real test, a tool would generate a complex payload.
class RCE
def initialize(cmd); @cmd = cmd; end
def to_s; `touch /tmp/pwned`; end
end
payload = Base64.encode64(Marshal.dump(RCE.new("")))
post "/api/v1/data", params: { data: payload }
# Assert that the malicious side-effect was not executed.
expect(File.exist?('/tmp/pwned')).to be false
end
end

