diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/utils/DefaultSerializeClassChecker.java b/dubbo-common/src/main/java/org/apache/dubbo/common/utils/DefaultSerializeClassChecker.java index 029f654abd26..52ac787cf61e 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/utils/DefaultSerializeClassChecker.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/utils/DefaultSerializeClassChecker.java @@ -123,6 +123,34 @@ public Class> loadClass(ClassLoader classLoader, String className) throws Clas return aClass; } + /** + * Check whether the given class satisfies the Serializable contract. + *
+ * This method is used when the target {@link Class} is already known (e.g. resolved from a + * method signature) so no class-name-based loading is required. It enforces the same + * Serializable check as {@link #loadClass} to close the gap where a generic invocation Map + * without a {@code "class"} key would otherwise bypass serialization security checks. + * + * @param clazz the class to validate + * @throws IllegalArgumentException if {@code checkSerializable} is {@code true} and the class + * does not implement {@link java.io.Serializable} + */ + public void checkClass(Class> clazz) { + if (clazz == null || clazz.isPrimitive() || Serializable.class.isAssignableFrom(clazz)) { + return; + } + String className = clazz.getName(); + String msg = "[Serialization Security] Serialized class " + className + + " has not implement Serializable interface. " + + "Current mode is strict check, will disallow to deserialize it by default. "; + if (serializeSecurityManager.getWarnedClasses().add(className)) { + logger.error(PROTOCOL_UNTRUSTED_SERIALIZE_CLASS, "", "", msg); + } + if (checkSerializable) { + throw new IllegalArgumentException(msg); + } + } + private Class> loadClass0(ClassLoader classLoader, String className) throws ClassNotFoundException { if (checkStatus == SerializeCheckStatus.DISABLE) { return classForName(classLoader, className); diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/utils/PojoUtils.java b/dubbo-common/src/main/java/org/apache/dubbo/common/utils/PojoUtils.java index e861c3f2987d..f126709596ff 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/utils/PojoUtils.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/utils/PojoUtils.java @@ -462,6 +462,11 @@ private static Object realize1( CLASS_NOT_FOUND_CACHE.put((String) className, NOT_FOUND_VALUE); } } + } else if (type != Object.class && !type.isInterface() && !Map.class.isAssignableFrom(type)) { + // When no "class" key is present, the target type is already resolved from the method + // signature. We must still enforce the Serializable contract to prevent bypassing the + // serialization security check via generic invocation without an explicit "class" entry. + DefaultSerializeClassChecker.getInstance().checkClass(type); } // special logic for enum diff --git a/dubbo-common/src/test/java/org/apache/dubbo/common/utils/PojoUtilsSerializableCheckTest.java b/dubbo-common/src/test/java/org/apache/dubbo/common/utils/PojoUtilsSerializableCheckTest.java new file mode 100644 index 000000000000..bce6165633ac --- /dev/null +++ b/dubbo-common/src/test/java/org/apache/dubbo/common/utils/PojoUtilsSerializableCheckTest.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.dubbo.common.utils; + +import org.apache.dubbo.rpc.model.FrameworkModel; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Regression tests for Issue #16270: + * Generic invocation without a {@code "class"} key in the argument Map must not bypass the + * Serializable check performed in {@link PojoUtils#realize1}. + * + *
Before the fix, Case A (no {@code "class"} key) silently succeeded even when the target
+ * POJO did not implement {@link java.io.Serializable}. After the fix both cases must be
+ * treated consistently: a non-Serializable POJO is rejected regardless of whether the caller
+ * included a {@code "class"} entry.
+ */
+class PojoUtilsSerializableCheckTest {
+
+ @BeforeEach
+ void setUp() {
+ FrameworkModel.destroyAll();
+ }
+
+ @AfterEach
+ void tearDown() {
+ FrameworkModel.destroyAll();
+ }
+
+ // -----------------------------------------------------------------------
+ // A concrete POJO that intentionally does NOT implement Serializable,
+ // matching the reproducer in Issue #16270.
+ // -----------------------------------------------------------------------
+ public static class NonSerializableDto {
+ private String label;
+ private Integer sequence;
+
+ public NonSerializableDto() {}
+
+ public String getLabel() {
+ return label;
+ }
+
+ public void setLabel(String label) {
+ this.label = label;
+ }
+
+ public Integer getSequence() {
+ return sequence;
+ }
+
+ public void setSequence(Integer sequence) {
+ this.sequence = sequence;
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Happy-path: Serializable POJO should still work in both map styles.
+ // -----------------------------------------------------------------------
+
+ /**
+ * A Map without a {@code "class"} key should succeed when the target type implements
+ * {@link java.io.Serializable}.
+ */
+ @Test
+ void realize_withoutClassKey_serializablePojo_shouldSucceed() {
+ Map