Deserialization without a class restriction

ID

kotlin.insecure_deserialization

Severity

low

Remediation Complexity

medium

Remediation Risk

medium

Remediation Effort

medium

Resource

Misconfiguration

Language

Kotlin

Tags

ASVS50:v1.5.2, CWE:502, NIST.SP.800-53, OWASP:2025:A08, PCI-DSS:6.5.1

Description

A serialized stream is deserialized without restricting the classes the stream may name, or a deserializer is configured to take the class to instantiate from the payload. Whoever writes the payload then chooses which classes are loaded and constructed, and any class on the classpath whose deserialization has a side effect — a gadget — becomes reachable.

Rationale

kotlin.code_injection_deserialization reports deserialization when a data path can be followed from an external input to the read. That is the right answer for application code, but it needs the input to be visible: a readObject() in a codec, a utility object or a shared library, where the caller lives in another project, has no source to follow and is reported by nothing.

The read is unsafe on its own terms, independent of who calls it. Its severity is low because the finding says nothing about whether an attacker-controlled payload actually reaches the call — when that can be shown, kotlin.code_injection_deserialization reports it as critical, and this finding is dropped as a duplicate.

What is reported

Kotlin runs on the JVM and uses the same libraries, so the sinks are those of java.insecure_deserialization:

API Note

ObjectInputStream.readObject() / readUnshared()

the core case, including local subclasses

java.beans.XMLDecoder.readObject()

always unsafe: the document is a call sequence

SerializationUtils.deserialize(..)

Commons Lang, wraps a plain ObjectInputStream

ObjectMapper.enableDefaultTyping(..)

Jackson, deprecated and unrestricted by design

ObjectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator..)

the validator accepts every subtype

Kryo.setRegistrationRequired(false)

the stream may name unregistered classes

The last three are reported where they are configured rather than where a document is read: that single call is both the defect and its fix, and the instance it produces is normally read from elsewhere.

Not covered: SnakeYAML and XStream

Both are deliberately absent, for the same reason they were removed from the Java rule. Each library fixed its default years ago — SnakeYAML 2.0 made the default tag inspector refuse global tags, XStream 1.4.18 replaced its denylist with a deny-by-default allowlist — so flagging them would be a false positive on any current dependency, and this detector cannot see which version a project resolves.

Every sink this rule does carry flags an explicit unsafe choice, which no library release can un-choose.

What counts as a restriction

A deserialization call is not reported when the restriction is visible in the same file: a per-stream setObjectInputFilter, the process-wide ObjectInputFilter.Config.setSerialFilter or jdk.serialFilter, a locally declared ObjectInputStream subclass overriding resolveClass, or a ValidatingObjectInputStream carrying an accept rule.

Nor is it reported when the read is the one a Java serialization callback was handed: readObject(ObjectInputStream) and readExternal(ObjectInput) run while the enclosing object is itself being deserialized, under whatever filter its caller installed. A stream a callback opens for itself is still reported.

Examples

Reported — the payload chooses the class:

fun decode(encoded: String): Any? {
    val raw = Base64.getDecoder().decode(encoded)
    val ois: ObjectInputStream = ObjectInputStream(ByteArrayInputStream(raw))
    return ois.readObject()                                     // reported
}

Not reported — the stream carries a class filter:

val ois: ObjectInputStream = ObjectInputStream(input)
ois.setObjectInputFilter(ObjectInputFilter.Config.createFilter("com.acme.dto.*;java.base/*;!*"))
return ois.readObject()

Remediation

The durable fix is not to accept serialized objects from anywhere untrusted: carry the data in a format that does not name classes — JSON or protobuf mapped onto a fixed type — and drop Java serialization from the boundary. Where the format cannot change, restrict the classes accepted:

val ois = ObjectInputStream(input)
ois.setObjectInputFilter(ObjectInputFilter.Config.createFilter("com.acme.dto.*;java.base/*;!*"))
return ois.readObject()

For the other libraries: XMLDecoder has no restriction to configure, so read only documents your own code produced; drop Jackson default typing or replace it with a BasicPolymorphicTypeValidator; leave Kryo’s setRegistrationRequired(true) and register the classes you expect.

Limitations

The restriction has to be visible in the same file, and the receiver is matched by the name it was written as. Kotlin infers most local types, so a receiver whose type the developer did not write is recognized only where the resolved type or the construction it is chained to names it.

© 2026 Xygeni. All rights reserved