diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/utils/SerializeSecurityConfigurator.java b/dubbo-common/src/main/java/org/apache/dubbo/common/utils/SerializeSecurityConfigurator.java index bd11fd8d1218..6382dff567bd 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/utils/SerializeSecurityConfigurator.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/utils/SerializeSecurityConfigurator.java @@ -45,6 +45,7 @@ import static org.apache.dubbo.common.constants.CommonConstants.SERIALIZE_ALLOW_LIST_FILE_PATH; import static org.apache.dubbo.common.constants.CommonConstants.SERIALIZE_BLOCKED_LIST_FILE_PATH; import static org.apache.dubbo.common.constants.LoggerCodeConstants.COMMON_IO_EXCEPTION; +import static org.apache.dubbo.common.constants.LoggerCodeConstants.COMMON_REFLECTIVE_OPERATION_FAILED; public class SerializeSecurityConfigurator implements ScopeClassLoaderListener { private static final ErrorTypeAwareLogger LOGGER = @@ -229,25 +230,34 @@ public synchronized void registerInterface(Class clazz) { checkClass(parameterType); } - Type[] genericParameterTypes = method.getGenericParameterTypes(); - for (Type genericParameterType : genericParameterTypes) { - checkType(genericParameterType); + try { + for (Type genericParameterType : method.getGenericParameterTypes()) { + checkType(genericParameterType); + } + } catch (LinkageError | TypeNotPresentException e) { + logScanFailure(clazz.getName() + "#" + method.getName() + " parameter types", e); } Class returnType = method.getReturnType(); checkClass(returnType); - Type genericReturnType = method.getGenericReturnType(); - checkType(genericReturnType); + try { + checkType(method.getGenericReturnType()); + } catch (LinkageError | TypeNotPresentException e) { + logScanFailure(clazz.getName() + "#" + method.getName() + " return type", e); + } Class[] exceptionTypes = method.getExceptionTypes(); for (Class exceptionType : exceptionTypes) { checkClass(exceptionType); } - Type[] genericExceptionTypes = method.getGenericExceptionTypes(); - for (Type genericExceptionType : genericExceptionTypes) { - checkType(genericExceptionType); + try { + for (Type genericExceptionType : method.getGenericExceptionTypes()) { + checkType(genericExceptionType); + } + } catch (LinkageError | TypeNotPresentException e) { + logScanFailure(clazz.getName() + "#" + method.getName() + " exception types", e); } } } @@ -266,28 +276,32 @@ private void checkType(Type type) { return; } - if (type instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType) type; - checkClass((Class) parameterizedType.getRawType()); - for (Type actualTypeArgument : parameterizedType.getActualTypeArguments()) { - checkType(actualTypeArgument); - } - } else if (type instanceof GenericArrayType) { - GenericArrayType genericArrayType = (GenericArrayType) type; - checkType(genericArrayType.getGenericComponentType()); - } else if (type instanceof TypeVariable) { - TypeVariable typeVariable = (TypeVariable) type; - for (Type bound : typeVariable.getBounds()) { - checkType(bound); - } - } else if (type instanceof WildcardType) { - WildcardType wildcardType = (WildcardType) type; - for (Type bound : wildcardType.getUpperBounds()) { - checkType(bound); - } - for (Type bound : wildcardType.getLowerBounds()) { - checkType(bound); + try { + if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + checkClass((Class) parameterizedType.getRawType()); + for (Type actualTypeArgument : parameterizedType.getActualTypeArguments()) { + checkType(actualTypeArgument); + } + } else if (type instanceof GenericArrayType) { + GenericArrayType genericArrayType = (GenericArrayType) type; + checkType(genericArrayType.getGenericComponentType()); + } else if (type instanceof TypeVariable) { + TypeVariable typeVariable = (TypeVariable) type; + for (Type bound : typeVariable.getBounds()) { + checkType(bound); + } + } else if (type instanceof WildcardType) { + WildcardType wildcardType = (WildcardType) type; + for (Type bound : wildcardType.getUpperBounds()) { + checkType(bound); + } + for (Type bound : wildcardType.getLowerBounds()) { + checkType(bound); + } } + } catch (LinkageError | TypeNotPresentException e) { + logScanFailure("generic type bounds or arguments", e); } } @@ -319,8 +333,12 @@ private boolean checkClass(Class clazz) { checkClass(interfaceClass); } - for (Type genericInterface : clazz.getGenericInterfaces()) { - checkType(genericInterface); + try { + for (Type genericInterface : clazz.getGenericInterfaces()) { + checkType(genericInterface); + } + } catch (LinkageError | TypeNotPresentException e) { + logScanFailure(className + " generic interfaces", e); } Class superclass = clazz.getSuperclass(); @@ -328,12 +346,21 @@ private boolean checkClass(Class clazz) { checkClass(superclass); } - Type genericSuperclass = clazz.getGenericSuperclass(); - if (genericSuperclass != null) { - checkType(genericSuperclass); + try { + checkType(clazz.getGenericSuperclass()); + } catch (LinkageError | TypeNotPresentException e) { + logScanFailure(className + " generic superclass", e); } - Field[] fields = clazz.getDeclaredFields(); + Field[] fields; + try { + fields = clazz.getDeclaredFields(); + } catch (LinkageError | TypeNotPresentException e) { + // Reflection resolves all raw field types before returning any Field objects. + // Skip this class's fields, but let callers continue scanning other reachable types. + logScanFailure(className + " fields", e); + return true; + } for (Field field : fields) { if (Modifier.isTransient(field.getModifiers())) { @@ -342,12 +369,27 @@ private boolean checkClass(Class clazz) { Class fieldClass = field.getType(); checkClass(fieldClass); - checkType(field.getGenericType()); + try { + checkType(field.getGenericType()); + } catch (LinkageError | TypeNotPresentException e) { + logScanFailure(className + "#" + field.getName(), e); + } } return true; } + private void logScanFailure(String source, Throwable cause) { + LOGGER.warn( + COMMON_REFLECTIVE_OPERATION_FAILED, + "", + "", + "Failed to inspect " + source + + " for the serialization allow list; continuing with other types. " + + "Check that the application's dependencies are available.", + cause); + } + private void addToAllow(Class clazz) { if (classHolder != null) { classHolder.storeClass(clazz); diff --git a/dubbo-common/src/test/java/com/serialization/MissingTypeFixtures.java b/dubbo-common/src/test/java/com/serialization/MissingTypeFixtures.java new file mode 100644 index 000000000000..50727a8f1d89 --- /dev/null +++ b/dubbo-common/src/test/java/com/serialization/MissingTypeFixtures.java @@ -0,0 +1,100 @@ +/* + * 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 com.serialization; + +import java.io.Serializable; +import java.util.List; + +/** + * Models loaded without OptionalDependency to exercise incomplete application classpaths. + * @date 2026-11-05 + */ +public class MissingTypeFixtures { + public static class OptionalDependency {} + + public static class Healthy implements Serializable { + public Nested value; + } + + public static class Nested implements Serializable {} + + public static class Unrelated implements Serializable {} + + public static class MissingField { + public OptionalDependency optional; + } + + public static class MissingTransientField { + public transient OptionalDependency optional; + } + + public static class MissingGenericField { + public List optional; + public Healthy healthy; + } + + public interface Contract {} + + public static class MissingGenericInterface implements Contract { + public Healthy healthy; + } + + public static class Base {} + + public static class MissingGenericSuperclass extends Base { + public Healthy healthy; + } + + public static class MissingBound { + public List optional; + public Healthy healthy; + } + + public interface FieldService { + Healthy read(MissingField value); + } + + public interface TransientFieldService { + Healthy read(MissingTransientField value); + } + + public interface GenericParameterService { + Healthy read(List value); + } + + public interface GenericFieldService { + void read(MissingGenericField value); + } + + public interface GenericInterfaceService { + void read(MissingGenericInterface value); + } + + public interface GenericSuperclassService { + void read(MissingGenericSuperclass value); + } + + public interface BoundService { + void read(MissingBound value); + } + + public interface GenericMethodService { + List optional(); + + Healthy read(); + } +} diff --git a/dubbo-common/src/test/java/org/apache/dubbo/common/utils/SerializeSecurityConfiguratorMissingTypeTest.java b/dubbo-common/src/test/java/org/apache/dubbo/common/utils/SerializeSecurityConfiguratorMissingTypeTest.java new file mode 100644 index 000000000000..81b2a04ab1f4 --- /dev/null +++ b/dubbo-common/src/test/java/org/apache/dubbo/common/utils/SerializeSecurityConfiguratorMissingTypeTest.java @@ -0,0 +1,112 @@ +/* + * 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.config.ApplicationConfig; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.FrameworkModel; + +import java.io.IOException; +import java.io.InputStream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Verifies that missing optional types do not prevent discovery of independent DTOs. + * @date 2026-11-05 + */ +class SerializeSecurityConfiguratorMissingTypeTest { + private static final String FIXTURES = "com.serialization.MissingTypeFixtures$"; + + @ParameterizedTest + @ValueSource( + strings = { + "FieldService", + "TransientFieldService", + "GenericParameterService", + "GenericFieldService", + "GenericInterfaceService", + "GenericSuperclassService", + "BoundService", + "GenericMethodService" + }) + void continuesScanningAfterMissingType(String serviceName) throws Exception { + ClassLoader loader = new MissingDependencyClassLoader(); + assertThrows(ClassNotFoundException.class, () -> loader.loadClass(FIXTURES + "OptionalDependency")); + Class service = loader.loadClass(FIXTURES + serviceName); + Class healthy = loader.loadClass(FIXTURES + "Healthy"); + Class nested = loader.loadClass(FIXTURES + "Nested"); + + FrameworkModel framework = new FrameworkModel(); + try { + ApplicationModel application = framework.newApplication(); + ApplicationConfig config = new ApplicationConfig("missing-type-test"); + config.setSerializeCheckStatus(SerializeCheckStatus.STRICT.name()); + config.setTrustSerializeClassLevel(Integer.MAX_VALUE); + application.getApplicationConfigManager().setApplication(config); + SerializeSecurityConfigurator configurator = new SerializeSecurityConfigurator(application.newModule()); + DefaultSerializeClassChecker checker = + framework.getBeanFactory().getBean(DefaultSerializeClassChecker.class); + + assertThrows(IllegalArgumentException.class, () -> checker.loadClass(loader, healthy.getName())); + assertDoesNotThrow(() -> configurator.registerInterface(service)); + assertEquals(healthy, checker.loadClass(loader, healthy.getName())); + assertEquals(nested, checker.loadClass(loader, nested.getName())); + assertThrows(IllegalArgumentException.class, () -> checker.loadClass(loader, FIXTURES + "Unrelated")); + assertDoesNotThrow(() -> configurator.registerInterface(service)); + } finally { + framework.destroy(); + } + } + + private static class MissingDependencyClassLoader extends ClassLoader { + MissingDependencyClassLoader() { + super(SerializeSecurityConfiguratorMissingTypeTest.class.getClassLoader()); + } + + @Override + protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.equals(FIXTURES + "OptionalDependency")) { + throw new ClassNotFoundException(name); + } + if (!name.startsWith(FIXTURES)) { + return super.loadClass(name, resolve); + } + Class loaded = findLoadedClass(name); + if (loaded == null) { + try (InputStream stream = getParent().getResourceAsStream(name.replace('.', '/') + ".class")) { + if (stream == null) { + throw new ClassNotFoundException(name); + } + byte[] bytes = IOUtils.toByteArray(stream); + loaded = defineClass(name, bytes, 0, bytes.length); + } catch (IOException e) { + throw new ClassNotFoundException(name, e); + } + } + if (resolve) { + resolveClass(loaded); + } + return loaded; + } + } +}