obabichevjb opened a new issue, #16491: URL: https://github.com/apache/dubbo/issues/16491
### Pre-check - [x] I am sure that all the content I provide is in English. ### Search before asking - [x] I had searched in the [issues](https://github.com/apache/dubbo/issues?q=is%3Aissue) 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`: ```kotlin 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: ```java package repro; import java.io.Serializable; public sealed interface JReply extends Serializable permits JFound {} ``` ```java 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`: ```kotlin 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? - [x] Yes I am willing to submit a pull request on my own! ### Code of Conduct - [x] I agree to follow this project's [Code of Conduct](https://www.apache.org/foundation/policies/conduct) -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
