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 map = new LinkedHashMap<>(); + map.put("name", "Alice"); + map.put("age", 30); + + // org.apache.dubbo.common.model.Person implements Serializable + org.apache.dubbo.common.model.Person result = (org.apache.dubbo.common.model.Person) + PojoUtils.realize(map, org.apache.dubbo.common.model.Person.class); + + Assertions.assertNotNull(result); + Assertions.assertEquals("Alice", result.getName()); + Assertions.assertEquals(30, result.getAge()); + } + + /** + * A Map with a {@code "class"} key pointing to a Serializable POJO should also succeed. + */ + @Test + void realize_withClassKey_serializablePojo_shouldSucceed() { + Map map = new LinkedHashMap<>(); + map.put("class", org.apache.dubbo.common.model.Person.class.getName()); + map.put("name", "Bob"); + map.put("age", 25); + + org.apache.dubbo.common.model.Person result = (org.apache.dubbo.common.model.Person) + PojoUtils.realize(map, org.apache.dubbo.common.model.Person.class); + + Assertions.assertNotNull(result); + Assertions.assertEquals("Bob", result.getName()); + } + + // ----------------------------------------------------------------------- + // Error-path: Non-Serializable POJO must be rejected in both map styles. + // ----------------------------------------------------------------------- + + /** + * Case B from Issue #16270: a Map with a {@code "class"} key referring to a + * non-Serializable type must throw {@link IllegalArgumentException}. + * (This already worked before the fix — kept here as a contract anchor.) + */ + @Test + void realize_withClassKey_nonSerializablePojo_shouldThrow() { + Map map = new LinkedHashMap<>(); + map.put("class", NonSerializableDto.class.getName()); + map.put("label", "with-class-key"); + map.put("sequence", 200); + + Assertions.assertThrows(IllegalArgumentException.class, () -> PojoUtils.realize(map, NonSerializableDto.class)); + } + + /** + * Case A from Issue #16270: a Map without a {@code "class"} key where the target + * type is resolved from the method signature must also throw {@link IllegalArgumentException}. + * This is the regression case introduced by the fix. + */ + @Test + void realize_withoutClassKey_nonSerializablePojo_shouldThrow() { + Map map = new LinkedHashMap<>(); + map.put("label", "no-class-key"); + map.put("sequence", 100); + + // Before the fix this silently succeeded. After the fix it must throw. + Assertions.assertThrows(IllegalArgumentException.class, () -> PojoUtils.realize(map, NonSerializableDto.class)); + } + + // ----------------------------------------------------------------------- + // Edge-path: interface / Object targets must not be blocked. + // ----------------------------------------------------------------------- + + /** + * When the declared type is {@link Object}, the new check must not fire (the map is + * returned as-is or wrapped, not instantiated as a POJO). + */ + @Test + void realize_withoutClassKey_objectType_shouldReturnMapAsIs() { + Map map = new LinkedHashMap<>(); + map.put("key", "value"); + + // Should not throw — Object target takes the Map branch, no POJO instantiation. + Object result = PojoUtils.realize(map, Object.class); + Assertions.assertNotNull(result); + } + + /** + * When the declared type is a Map, the new check must not fire. + */ + @Test + void realize_withoutClassKey_mapType_shouldReturnMap() { + Map map = new LinkedHashMap<>(); + map.put("k", "v"); + + @SuppressWarnings("unchecked") + Map result = (Map) PojoUtils.realize(map, Map.class); + Assertions.assertNotNull(result); + Assertions.assertEquals("v", result.get("k")); + } +}