Dubbo3序列化安全机制导致的一次生产故障

前言

记录一次 Dubbo 线上故障排查和原因分析。

线上 Dubbo 消费者启动有错误日志如下,但是不影响服务启动。

java 复制代码
java.lang.TypeNotPresentException: Type org.example.model.ThirdParam not present
...
Caused by: java.lang.ClassNotFoundException: org.example.model.ThirdParam
...

紧接着,消费者发起 RPC 调用,偶发性报错如下:

java 复制代码
Caused by: org.apache.dubbo.remoting.RemotingException: 
Failed to send message Request [id=-7672337589162309142, version=2.0.2, twoWay=true, event=false, broken=false, mPayload=0, data=null] to /192.168.98.92:20880, 
cause: org.apache.dubbo.common.serialize.SerializationException: 
java.lang.IllegalArgumentException: [Serialization Security] 
Serialized class org.example.api.InnerParam 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.

消费者尝试重启,RPC 调用偶尔成功,偶尔失败,没有规律。

故障重现

为了重现故障,我写了个示例工程,有四个模块:

  • third-sdk

模拟依赖的三方 SDK,有两个版本:V1.0 V2.0,区别是 ThirdParam 类只在 V2.0 才提供。

  • dubbo-api

Dubbo API 模块,包含服务接口和参数类,依赖third-sdk V2.0

  • dubbo-provider

Dubbo 服务提供者,直接依赖dubbo-api,间接依赖third-sdk V2.0

  • dubbo-consumer

Dubbo 服务消费者,直接依赖dubbo-api,直接依赖third-sdk V1.0

重点:dubbo-consumer 模块自身依赖了低版本的 **<font style="color:#DF2A3F;">third-sdk</font>**,ThirdParam 类是不存在的。

dubbo-api模块,IService 接口如下。InnerParam 类存在于当前模块,ThirdParam 来自三方库。

由于dubbo-consumer模块依赖的是低版本的third-sdk,所以 ThirdParam 类不存在,但只要不调用 M2 就没事。

java 复制代码
public interface IService {
    String M1(InnerParam innerParam);

    String M2(Optional<ThirdParam> optional);
}

ServiceImpl.java和服务提供者的启动和消费者的调用均不是重点,这里不贴代码。

说明:为啥 M2 参数类型是Optional<ThirdParam>

因为如果参数类型直接是 ThirdParam,消费者启动时,解析 Service Class 这一步就会因为 Class Not Found 直接报错而退出进程。ThirdParam 必须是泛型,才不至于 Service Class 无法解析。

接着,启动 Provider,启动 Consumer,就能看到错误日志,但是不影响消费者启动。

再接着,Consumer 发起 RPC 调用,就会报错:

最快的修复方式,重构 IService.java ,方法名 M1 改为 a1

java 复制代码
public interface IService {
    String a1(InnerParam innerParam);

    String M2(Optional<ThirdParam> optional);
}

接着,重启 Provider,Consumer。Consumer 启动依然有错误日志,但是不影响启动。

Consumer 发起 a1 的 RPC 调用,成功,不再报错。

java 复制代码
call a1 start...
call a1 result: OK

为什么仅仅修改个方法名,RPC 调用就不再报错了呢?

故障分析

已知,Dubbo 从 3.1.6 版本开始,为了避免序列化引起的 RCE 攻击,引入了"序列化类检查机制"。只有在信任白名单里的类,才允许被序列化和反序列化。

同时,为了避免开发者手动添加白名单带来的额外负担,Dubbo 默认开启"自动信任机制"。即 Dubbo 会在 Service 暴露和引用的同时,自动信任 Service Class 依赖的相关类,这些类包括:Service Class 本身、父类和接口类型、属性类型、方法的所有入参/出参类型、异常类型等,将它们全部加入到白名单里。

根据消费者报错的信息来看,很明显提示org.example.api.InnerParam类不在白名单里面,所以序列化失败。

java 复制代码
cause: org.apache.dubbo.common.serialize.SerializationException: 
java.lang.IllegalArgumentException: [Serialization Security] 
Serialized class org.example.api.InnerParam is not in allow list. 

由此我们推测,Dubbo 的"自动信任机制"出现了问题

通过源码我们发现,Service 在暴露和引用的时候,默认会注册 Service Class,方法是SerializeSecurityConfigurator#registerInterface

注册接口就是将 Service Class 自身、以及超类、属性类、方法的入参/出参、返回类型、异常类型等通通加入到白名单。

java 复制代码
public synchronized void registerInterface(Class<?> clazz) {
    /**
     * 是否自动信任序列化类?默认是true
     * 默认会将 Service Class 涉及到的类加入白名单,全部信任
     */
    if (!autoTrustSerializeClass) {
        return;
    }

    Set<Type> markedClass = new HashSet<>();
    /**
     * 1. 信任 Service Class 自身
     * 2. 根据 TrustSerializeClassLevel 信任所在包的层级
     * 3. 信任 Service Class 的接口、父类、属性类型、
     */
    checkClass(markedClass, clazz);

    addToAllow(clazz.getName());

    Method[] methodsToExport = clazz.getMethods();

    // 信任 Service Class 方法的入参、出参类型、抛出的异常类型
    for (Method method : methodsToExport) {
        Class<?>[] parameterTypes = method.getParameterTypes();
        for (Class<?> parameterType : parameterTypes) {
            checkClass(markedClass, parameterType);
        }

        Type[] genericParameterTypes = method.getGenericParameterTypes();
        for (Type genericParameterType : genericParameterTypes) {
            checkType(markedClass, genericParameterType);
        }

        Class<?> returnType = method.getReturnType();
        checkClass(markedClass, returnType);

        Type genericReturnType = method.getGenericReturnType();
        checkType(markedClass, genericReturnType);

        Class<?>[] exceptionTypes = method.getExceptionTypes();
        for (Class<?> exceptionType : exceptionTypes) {
            checkClass(markedClass, exceptionType);
        }

        Type[] genericExceptionTypes = method.getGenericExceptionTypes();
        for (Type genericExceptionType : genericExceptionTypes) {
            checkType(markedClass, genericExceptionType);
        }
    }
}

Dubbo 会遍历 Service Class 所有方法,依次注册方法的入参、出参到白名单。

问题就出在这个遍历上,因为dubbo-consumer模块直接依赖了third-sdk V1.0,对于方法IService#M2(Optional<ThirdParam>)的入参,ThirdParam 类是不存在的,导致整个注册过程中断跳出,后续方法的参数都没有注册到白名单,进而导致 Consumer 发起 RPC 调用时,参数序列化报错。

另一个问题,为什么方法 **IService#M1**重构为 **IService#a1**,Consumer 就正常了呢?

这是因为方法签名修改后,导致Class#getMethods返回的 Method 顺序发生了改变,如果a1方法先于M2方法返回,让中断发生在a1方法注册之后,虽然整个注册过程还是会异常,但是org.example.api.InnerParam类已经添加到白名单了,对后续的 RPC 调用当然没有影响。

注意:虽然示例中通过修改方法名来改变 **Class#getMethods**返回的 Method 顺序,但是强烈不建议这么做,因为 Java Doc 已经写的非常清楚了,返回的方法数组没有特定顺序,取决于JVM实现。

java 复制代码
The elements in the returned array are not sorted and are not in any particular order.

推荐的修复方式,Service Class 所有的方法入参和出参,都不应该直接用三方 SDK 的类,这本身就不规范。在 API 模块新建 DTO 类,把三方类转换成自己的 DTO 类。

尾巴

因为消费者模块和公共 API 模块依赖的三方库版本不同,导致消费者模块缺少一部分类,进而导致消费者在注册 Service Class 方法参数到序列化白名单时,发生异常中断跳出,没有被信任的参数类,一旦序列化就会抛出异常。

又因为Class#getMethods返回的方法顺序并不固定,就会导致方法参数偶尔被信任,偶尔不被信任,所以会出现服务重启后可能又恢复正常的错觉。

相关推荐
橘颂TA15 小时前
【测试】自动化测试函数介绍——web 测试
python·功能测试·selenium·测试工具·dubbo
wc8821 小时前
dubbo本地直连一个程序既当提供者又当消费者问题
dubbo
小信丶3 天前
@Activate 注解详解:应用场景与实战示例
java·spring boot·后端·spring·spring cloud·微服务·dubbo
不像程序员的程序媛3 天前
nacos作为dubbo服务注册中心
dubbo
BraveHeart!5 天前
@DubboService 与 @DubboReference 初始化过程
dubbo
信创天地7 天前
从 “替代” 到 “超越”:信创系统架构师如何筑牢自主可控技术底座
运维·安全·系统架构·开源·dubbo·risc-v
wniuniu_7 天前
运维运维运维
java·运维·dubbo
量子炒饭大师8 天前
【C++入门】Cyber骇客构造器的核心六元组 —— 【类的默认成员函数】明明没写构造函数也能跑?保姆级带你掌握六大类的默认成员函数(上:函数篇)
开发语言·c++·dubbo·默认成员函数
信创天地8 天前
信创运维核心技术:国产化软硬件适配与故障排查全解析
运维·人工智能·开源·dubbo·运维开发·risc-v
bing.shao9 天前
文心大模型 5.0 正式版上线:用 Golang 解锁全模态 AI 工业化落地新路径
人工智能·golang·dubbo