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返回的方法顺序并不固定,就会导致方法参数偶尔被信任,偶尔不被信任,所以会出现服务重启后可能又恢复正常的错觉。

相关推荐
拾忆,想起13 小时前
Dubbo vs Spring Cloud Gateway:本质剖析与全面对比指南
微服务·性能优化·架构·dubbo·safari
java_logo1 天前
LinuxServer.io LibreOffice 容器化部署指南
java·开发语言·docker·dubbo·openoffice·libreoffice·opensource
拾忆,想起1 天前
Dubbo多协议暴露完全指南:让一个服务同时支持多种通信方式
xml·微服务·性能优化·架构·dubbo
拾忆,想起3 天前
Dubbo服务调用幂等性深度解析:彻底解决重复请求的终极方案
微服务·性能优化·服务发现·dubbo
拾忆,想起3 天前
Dubbo深度解析:从零到一,高性能RPC框架如何重塑微服务架构
网络协议·微服务·云原生·性能优化·rpc·架构·dubbo
武子康3 天前
Java-194 RabbitMQ 分布式通信怎么选:SOA/Dubbo、微服务 OpenFeign、同步重试与 MQ 异步可靠性落地
大数据·分布式·微服务·消息队列·rabbitmq·dubbo·异步
拾忆,想起4 天前
Dubbo服务依赖问题终结指南:从根因分析到系统化解决方案
微服务·性能优化·架构·dubbo·safari
拾忆,想起4 天前
Dubbo通信协议全景指南:如何为你的微服务选择最佳通信方案?
微服务·云原生·性能优化·架构·dubbo·safari
永不停歇的蜗牛5 天前
K8S之Ctr 和 Docker的区别
docker·kubernetes·dubbo
apihz5 天前
反向DNS查询与蜘蛛验证免费API接口详细教程
android·开发语言·数据库·网络协议·tcp/ip·dubbo