logo

Database

Need

Avoidance of native Java deserialization of untrusted data

Context

• Usage of Kotlin 1.9+ on the JVM for building application services

• Usage of Java object serialization

• Usage of the Jackson Kotlin module for JSON

Description

1. Non compliant code

import java.io.InputStream
import java.io.ObjectInputStream
import java.io.Serializable

class UserProfile(val username: String, val isAdmin: Boolean) : Serializable

fun readProfile(body: InputStream): UserProfile =
    // Any serializable class on the classpath can be instantiated here...

The `readProfile` function below restores a `UserProfile` object from the bytes of a request body with `ObjectInputStream.readObject()`. Java deserialization reconstructs any serializable class that appears in the stream, not only the expected one, and runs code from those classes while doing it. An attacker who controls the bytes can send a "gadget chain" built from classes that happen to be on the classpath, such as those of Apache Commons Collections or Spring, which ends in arbitrary command execution before the cast to `UserProfile` is even reached. Tools such as ysoserial generate these payloads automatically. The same stream also lets the caller set any field of the object, such as `isAdmin`, without going through the validation of the application.

2. Steps

• Do not call `ObjectInputStream.readObject` on data that comes from clients, files uploaded by users or other untrusted sources.

• Exchange data as JSON or another data-only format, mapped to explicit Kotlin data classes.

• Keep Jackson polymorphic default typing disabled, and never select classes from type names in the payload.

• Decide privileges and other security attributes on the server instead of reading them from client data.

• If native deserialization must remain, configure an `ObjectInputFilter` that only allows the expected classes.

3. Secure code example

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import java.io.InputStream

data class ProfileRequest(val username: String)

// Plain data classes only; default typing stays disabled...

The corrected function reads the profile as JSON with Jackson and the Kotlin module, mapping it to a data class. JSON deserialization only creates `ProfileRequest`, with the fields it declares, so there is no way to make the server instantiate arbitrary classes. The request type no longer contains `isAdmin`: privileges are decided by the server, never read from client data. `FAIL_ON_UNKNOWN_PROPERTIES` rejects payloads with extra fields, and polymorphic typing, which would reintroduce class selection from the payload, is not enabled. When native serialization cannot be removed, `ObjectInputFilter` with an allowlist of expected classes limits what `readObject` may create, but it is a mitigation, not a replacement for a data format.