jan-karel.com
Home / Security Measures / Web Security / Deserialization Prevention

Deserialization Prevention

Deserialization Prevention

Deserialization Prevention

Objects Without Surprises

Secure web development is not about extra friction, but about better defaults in design, code, and release flow.

With Deserialization Prevention, the greatest gains come from safe defaults that are automatically enforced in every release.

This makes security less of a separate afterthought check and more of a standard quality of your product.

Immediate measures (15 minutes)

Why this matters

The core of Deserialization Prevention is risk reduction in practice. Technical context supports the choice of measures, but implementation and assurance are central.

Defense

General principles

1. Never deserialize untrusted data with type information

This is the golden rule. If you receive data from a user and that data determines which type of object gets created, then you have a problem. Use formats that do not contain type information (JSON without TypeNameHandling, protobuf with a fixed schema) or strictly validate which types are allowed.

2. Whitelist classes

If you absolutely must deserialize with type information, use a whitelist of allowed classes. Not a blacklist -- that is always incomplete.

Java (serialization filter, JEP 290):

// Only allow specific classes
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.myapp.model.*;!*"
);
ObjectInputStream ois = new ObjectInputStream(stream);
ois.setObjectInputFilter(filter);

.NET:

// Use System.Text.Json instead of Newtonsoft.Json
// Or if you must use Newtonsoft:
var settings = new JsonSerializerSettings {
    TypeNameHandling = TypeNameHandling.None  // NEVER Auto/Objects/All
};

PHP:

<?php
// Use allowed_classes parameter (PHP 7+)
$obj = unserialize($data, ['allowed_classes' => ['User', 'Product']]);

// Or better: do not use unserialize() on user input at all
// Use JSON:
$data = json_decode($input, true);

3. Integrity checks

Add an HMAC (Hash-based Message Authentication Code) to serialized data. If the data has been modified, you detect that before deserialization:

// Before serialization
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(secretKey);
byte[] signature = mac.doFinal(serializedData);
// Send serializedData + signature

// Before deserialization
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(secretKey);
byte[] expectedSignature = mac.doFinal(receivedData);
if (!MessageDigest.isEqual(expectedSignature, receivedSignature)) {
    throw new SecurityException("Data has been modified!");
}

4. Remove unnecessary gadgets from the classpath

In Java: if you are not actively using Apache Commons Collections, remove it from your classpath. No gadgets on the classpath = no gadget chains.

5. Upgrade

Many of these vulnerabilities have been fixed in newer versions: - PHP 8.0 has removed most type juggling behavior - lodash.merge filters __proto__ since version 4.6.2 - .NET's BinaryFormatter is deprecated and removed in .NET 9 - Java's serialization filters (JEP 290) have been available since Java 9

Language-specific defense

Java:

// DON'T:
ObjectInputStream ois = new ObjectInputStream(untrustedStream);
Object obj = ois.readObject();

// DO: use JSON (Jackson, Gson) with POJOs
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
// Deserialize to a specific type, not to Object
User user = mapper.readValue(jsonString, User.class);

.NET:

// DON'T:
BinaryFormatter bf = new BinaryFormatter();
object obj = bf.Deserialize(stream);

// DON'T:
var settings = new JsonSerializerSettings {
    TypeNameHandling = TypeNameHandling.Auto
};

// DO: System.Text.Json (no TypeNameHandling)
var user = JsonSerializer.Deserialize<User>(jsonString);

PHP:

<?php
// DON'T:
$obj = unserialize($_COOKIE['session']);

// DO: JSON
$data = json_decode($_COOKIE['session'], true);

// DON'T:
if (md5($input) == $stored_hash) { ... }

// DO: strict comparison and password_verify
if (password_verify($input, $stored_hash)) { ... }

JavaScript:

// DON'T: vulnerable deep merge
function merge(target, source) {
    for (let key in source) {
        target[key] = source[key];  // Polluted!
    }
}

// DO: filter prototype keys
function safeMerge(target, source) {
    for (let key of Object.keys(source)) {
        if (key === '__proto__' || key === 'constructor') continue;
        if (typeof source[key] === 'object' && source[key] !== null) {
            target[key] = target[key] || {};
            safeMerge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
}

// OR: use Object.create(null) for prototype-less objects
const safeStore = Object.create(null);

// OR: use Map
const safeMap = new Map();

Reference table

Deserialization risks per language

Language Sink Detection Gadget tool Defense
Java ObjectInputStream.readObject() AC ED 00 05 / rO0AB ysoserial JEP 290 filters, whitelist classes
.NET BinaryFormatter.Deserialize() Base64 in ViewState/cookies ysoserial.net System.Text.Json, no TypeNameHandling
PHP unserialize() O: prefix in data PHPGGC json_decode(), allowed_classes
Python pickle.loads() \x80\x05 header -- json.loads(), never pickle on user input
Ruby Marshal.load() \x04\x08 header -- JSON.parse()

Magic bytes for detection

Format Hex Base64 prefix
Java serialized AC ED 00 05 rO0ABQ
.NET BinaryFormatter Variable Variable (check ViewState)
Python pickle (v5) 80 05 gAU
Ruby Marshal (4.8) 04 08 BAg
PHP serialized Readable: O:, a:, s: N/A (text)

PHP type juggling cheat sheet

Expression Result Why
"0e123" == "0e456" true Both are 0 in scientific notation
true == "anything" true Bool true == any non-empty string
0 == "php" true (< 8.0) "php" cast to int 0
"" == null true Empty string is null-like
"0" == false true String "0" is falsy
[] == null false Array is not null
strcmp([], "str") NULL strcmp crashes on arrays
NULL == 0 true NULL cast to int 0

Prototype pollution RCE chains

Template engine Polluted property Impact
EJS outputFunctionName RCE via template render
Pug block RCE via template compile
Handlebars pendingContent RCE via compiler
Nunjucks env RCE via environment

Tools

Tool URL Use
ysoserial https://github.com/frohoff/ysoserial Java deserialization payloads
ysoserial.net https://github.com/pwntester/ysoserial.net .NET deserialization payloads
PHPGGC https://github.com/ambionics/phpggc PHP gadget chain payloads
marshalsec https://github.com/mbechler/marshalsec Various marshalling formats
JNDI safety checklist Internal guidelines Safe parser and lookup configuration

Summary

Deserialization is the problem that arises when we forget that not all suitcases are packed by friends. We take binary blobs, JSON with type hints, and PHP strings from the internet, and we build objects from them in the memory of our server. Objects that have methods. Objects that do things.

Java's gadget chains show how existing libraries can be chained together into a remote code execution machine. .NET's BinaryFormatter and ViewState prove that the problem is not language-bound. PHP's type juggling demonstrates that you do not even need deserialization -- a == instead of === is enough to bypass authentication. And JavaScript's prototype pollution shows that enriching the prototype chain of all objects in an application is just one __proto__ property away.

The defense is conceptually simple: do not trust user data to construct objects. Use type-safe formats without class information. Whitelist what may be deserialized. Validate integrity with HMACs. And if you write PHP, use ===. Always. Everywhere. No exceptions.

Op de hoogte blijven?

Ontvang maandelijks cybersecurity-inzichten in je inbox.

← Web Security ← Home