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 |
|---|---|
|
the core case, including local subclasses |
|
always unsafe: the document is a call sequence |
|
Commons Lang, wraps a plain |
|
Jackson, deprecated and unrestricted by design |
|
the validator accepts every subtype |
|
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.