From cf032ccf6166525f7cb1d8ef703ec196d2cb1f35 Mon Sep 17 00:00:00 2001 From: IceSource Date: Thu, 5 Nov 2026 10:39:51 +0800 Subject: [PATCH] fix: isolate missing types during serialization allow-list scanning Continue independent reflection branches when optional field or generic types cannot be resolved. Preserve STRICT checks and cover missing raw fields, transient fields, generic metadata, and bounds with an isolated class loader. Fixes #16574 --- .../utils/SerializeSecurityConfigurator.java | 114 ++++++++++++------ .../serialization/MissingTypeFixtures.java | 100 +++++++++++++++ ...zeSecurityConfiguratorMissingTypeTest.java | 112 +++++++++++++++++ 3 files changed, 290 insertions(+), 36 deletions(-) create mode 100644 dubbo-common/src/test/java/com/serialization/MissingTypeFixtures.java create mode 100644 dubbo-common/src/test/java/org/apache/dubbo/common/utils/SerializeSecurityConfiguratorMissingTypeTest.java 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; + } + } +}