Skip to content

[Bug] Auto-trusted serialization allow list skips the subtypes of a sealed parameter or return type #16491

Open

Description

@obabichevjb

Pre-check

  • I am sure that all the content I provide is in English.

Search before asking

  • I had searched in the issues and found no similar issues.

Apache Dubbo Component

Java SDK (apache/dubbo)

Dubbo Version

Dubbo Java 3.3.6, OpenJDK 21 (Amazon Corretto 21.0.5), macOS 15.6 (OS-independent)
Kotlin 2.4.20 / Java 17+ sealed types, Gradle 9.5, default hessian2 serialization, STRICT mode

Steps to reproduce this issue

SerializeSecurityConfigurator.registerInterface auto-allow-lists the classes it can reach from an
exported interface's signatures. checkClass walks up into supertypes and into fields and
generic arguments, but never down into subtypes. A sealed parameter or return type therefore
gets the parent trusted and none of the subtypes that actually travel on the wire.

This is the default configuration — autoTrustSerializeClass defaults to true and the check
status to STRICT — and no security/serialize.allowlist file is supplied below.

Provider and consumer run in one JVM over a real Netty dubbo:// transport with a direct URL and
RegistryConfig("N/A"), so no registry or other infrastructure is needed; because the transport is
real rather than injvm://, values are genuinely serialized.

build.gradle.kts:

plugins {
    kotlin("jvm") version "2.4.20"
    application
}
repositories { mavenCentral() }
dependencies {
    implementation("org.apache.dubbo:dubbo:3.3.6")
    implementation("io.netty:netty-all:4.1.118.Final")
    implementation("org.javassist:javassist:3.30.2-GA")
    implementation("org.slf4j:slf4j-simple:2.0.13")
}
kotlin { jvmToolchain(21) }
application { mainClass = "repro.MainKt" }

src/main/java/repro/JReply.java and JFound.java — a Java 17 sealed hierarchy, to show this is
not Kotlin-specific:

package repro;

import java.io.Serializable;

public sealed interface JReply extends Serializable permits JFound {}
package repro;

public final class JFound implements JReply {
    private String text;

    public JFound() {}

    public JFound(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    @Override
    public String toString() {
        return "JFound(" + text + ")";
    }
}

src/main/kotlin/Main.kt:

package repro

import java.io.Serializable
import org.apache.dubbo.config.*
import org.apache.dubbo.config.bootstrap.DubboBootstrap

sealed interface Reply : Serializable

data class Found(val text: String) : Reply

data object NotFound : Reply

interface ReplyService {
    fun lookup(key: String): Reply       // returns Found or NotFound
    fun javaLookup(key: String): JReply  // Java 17 sealed, same shape
}

class ReplyServiceImpl : ReplyService {
    override fun lookup(key: String): Reply = if (key == "x") Found("hit") else NotFound
    override fun javaLookup(key: String): JReply = JFound("hit")
}

private fun attempt(label: String, call: () -> Any) {
    try {
        println("$label -> OK: ${call()}")
    } catch (t: Throwable) {
        val root = generateSequence(t) { it.cause }.last()
        println("$label -> ${root::class.java.name}: ${root.message?.lines()?.first()}")
    }
}

fun main() {
    DubboBootstrap.getInstance()
        .application(ApplicationConfig("sealed-repro"))
        .registry(RegistryConfig("N/A"))
        .protocol(ProtocolConfig("dubbo", 20883))
        .service(ServiceConfig<ReplyService>().apply {
            setInterface(ReplyService::class.java)
            setRef(ReplyServiceImpl())
        })
        .start()

    val svc = ReferenceConfig<ReplyService>().apply {
        setInterface(ReplyService::class.java)
        url = "dubbo://127.0.0.1:20883"
    }.get()

    println("Dubbo ${org.apache.dubbo.common.Version.getVersion()}, no security/serialize.allowlist supplied")
    attempt("kotlin data class  (subtype of sealed Reply) ") { svc.lookup("x") }
    attempt("kotlin data object (subtype of sealed Reply) ") { svc.lookup("y") }
    attempt("java 17 sealed     (subtype of sealed JReply)") { svc.javaLookup("x") }

    DubboBootstrap.getInstance().destroy()
}

gradle run prints (long lines wrapped, otherwise verbatim):

Dubbo 3.3.6, no security/serialize.allowlist supplied
kotlin data class  (subtype of sealed Reply)  -> org.apache.dubbo.remoting.RemotingException:
  Failed to send response: ... result=AppResponse [value=Found(text=hit), exception=null]],
  cause: java.io.IOException: org.apache.dubbo.common.serialize.SerializationException:
  java.lang.IllegalArgumentException: [Serialization Security] Serialized class repro.Found is
  not in allow list. Current mode is `STRICT`, will disallow to deserialize it by default.
  Please add it into security/serialize.allowlist or follow FAQ to configure it.
kotlin data object (subtype of sealed Reply)  -> ... Serialized class repro.NotFound is not in allow list ...
java 17 sealed     (subtype of sealed JReply) -> ... Serialized class repro.JFound is not in allow list ...

Reply, JReply and ReplyService are all in the allow list. Found, NotFound and JFound are
not. Note that the check fires while the provider serializes the response, so the consumer sees
a RemotingException wrapping the IllegalArgumentException rather than a deserialization failure.

A larger demo exists that sends one payload down each route by which auto-trust can reach a class,
with Java controls, so the pattern is isolated rather than inferred:

route to payload           | declared as    | gate hit
---------------------------+----------------+--------------
direct param/return        | DirectPlain    | SERIALIZABLE   (not Serializable; a Java POJO behaves the same)
direct param/return        | DirectSerial.  | --             (trusted)
field of param/return      | Envelope       | --             (trusted)
generic type argument      | List<Listed>   | --             (trusted)
subtype of sealed          | Reply          | ALLOWLIST      <-- this report (Kotlin data class)
subtype of sealed          | Reply          | ALLOWLIST      <-- this report (Kotlin data object)
subtype of sealed          | JSealedReply   | ALLOWLIST      <-- this report (Java 17 sealed)
subtype of interface       | JReply (Java)  | ALLOWLIST      (non-sealed: cannot be enumerated)

Only the subtype route fails. Direct, field and generic routes all pass, and nothing about Kotlin or
data class varies between those rows.

What you expected to happen

The subtypes a sealed type permits should be auto-trusted along with the parent.

A sealed type enumerates its subtypes in its own source, and the JVM refuses to load any subtype
absent from that list. Trusting them therefore grants exactly the set the author declared and
nothing an attacker can extend — a more precise grant than the package-prefix trust Dubbo already
applies when trustSerializeClassLevel is set. A non-sealed hierarchy is open, cannot be
enumerated, and must keep behaving as it does today.

What happens instead is that the developer has to list every subtype by hand, repeat that for every
sealed hierarchy, and remember to update it whenever a subtype is added — for a set the compiler
already knows in full.

Anything else

No response

Do you have a (mini) reproduction demo?

  • Yes, I have a minimal reproduction demo to help resolve this issue more effectively!

Are you willing to submit a pull request to fix on your own?

  • Yes I am willing to submit a pull request on my own!

Code of Conduct

Activity

  1. LI123456mo commented on Oct 8, 2026

    @LI123456mo
    Contributor

    Good work , you really dig up this one

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions