Dubbo 序列化类检查和自动信任机制

前言

Dubbo 框架采用"微内核+插件"的开发机制,序列化就是以插件的形式存在的。

Dubbo 支持很多常用的序列化方式,例如:protobuf、hessian2、kryo、fastjson2 等。

需要注意的是,这些序列化方式并不总是安全的,因为序列化而爆出的漏洞时有发生。在 Dubbo 内置的序列化中,protobuf 具有最高的安全性,而对于其他序列化机制而言,要防止因为序列化引发的 RCE 攻击。

本文将记录 Dubbo 在避免因为序列化而引发的攻击所做的努力。

RCE攻击

RCE(Remote Code Execution)指"远程代码执行"攻击,是一种常见的网络安全攻击方式。攻击者利用软件中的漏洞,在目标主机上远程执行恶意代码,达到攻击的目的。

例如,攻击者通过向数据中注入恶意代码,利用反序列化漏洞来执行代码,这属于数据来源不可信。

再例如,在 Java 环境中,三方提供了一些恶意的类,利用序列化的漏洞来进行数据窃取、篡改等操作,这属于序列化类本身不可信。

这里以 Dubbo 默认的 hessian2 序列化为例,通过反序列化一个危险的 HashMap 子类,直接让 JVM 退出。

Hessian2ObjectInput#readObject用于将字节序列反序列化成 Java 对象,对于 Map 类型,它调用的是MapDeserializer#readMap,如下所示:

最终实例化 Map 示例,并调用put方法,如下所示:

有了这个前提,我们就可以构建一个危险的 HashMap 子类,重写 put 方法来攻击。如下所示,重写后的 put 方法直接关闭 JVM。

java 复制代码
public class DangerousMap<K, V> extends HashMap<K, V> {
    @Override
    public V put(K key, V value) {
        System.out.println("The JVM will be shutdown...");
        System.exit(-1);
        return super.put(key, value);
    }
}

测试 DangerousMap 的反序列化,你将看不到 end...输出,JVM 会直接关闭。细思极恐,仅仅是反序列化一个 Map 对象,竟然导致 JVM 退出,可见序列化不被信任的 Class 要非常小心。

java 复制代码
public class SerializationTest {
    public static void main(String[] args) throws Exception {
        FrameworkModel frameworkModel = new FrameworkModel();
        URL url = URL.valueOf("").setScopeModel(frameworkModel);

        // 通过SPI加载hessian2序列化器
        Serialization serialization = frameworkModel.getExtensionLoader(Serialization.class).getExtension("hessian2");
        // 序列化
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ObjectOutput objectOutput = serialization.serialize(url, outputStream);
        Map<String, String> map = new HashMap<>();
        map.put("a", "1");
        objectOutput.writeObject(map);
        objectOutput.flushBuffer();

        // 反序列化
        ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
        DangerousMap dm = serialization.deserialize(url, inputStream).readObject(DangerousMap.class);
        System.out.println(dm);
        System.out.println("end...");
    }
}

类检查机制

Dubbo 从 3.1.6 版本开始,引入"序列化类检查机制",避免因为不受限制的类型序列化引起的风险。

为了升级的平滑过渡,Dubbo 支持三种"类检查机制等级":

  • DISABLE:禁用类检查机制
  • WARN:警告级别,3.1 版本的默认值,允许序列化不被信任的类,但会有警告日志
  • STRICT:严格级别,3.2 版本的默认值,禁止序列化不被信任的类,并抛出异常

检查等级可通过dubbo.properties配置

properties 复制代码
dubbo.application.serialize-check-status=STRICT

在严格模式下,为了序列化不报错,可以通过security/serialize.allowlist配置信任的类,通过security/serialize.blockedlist配置不信任的类。

配置内容是类名或包名,例如:

properties 复制代码
org.example.echo.pojo

在 WARN 级别下,遇到不信任的序列化类型,控制台会出现如下所示的警告:

在 STRICT 级别下,遇到不信任的序列化类型,Dubbo 会直接抛出异常,如下所示:

自动信任机制

严格模式下,可序列化的类必须是信任的,而信任必须通过security/serialize.allowlist进行配置,这对开发者很不友好,太麻烦了。所以,Dubbo 默认支持"自动信任"机制。

配置AutoTrustSerializeClass=true 即可开启"自动信任",默认也是开启的。

开启自动信任机制后,Dubbo 会在 Service 暴露和引用的同时,自动信任 Service Class 依赖的相关类,这些类包括:Service Class 本身、父类和接口类型、属性类型、方法的所有入参/出参类型、异常类型等,将它们全部加入到信任白名单里面,省去了开发者自行配置的繁琐步骤。

由此可见,开启类型检查机制,既保证了安全,一般情况下,开发者无需额外工作,Dubbo 程序也能正常运行。

配置TrustSerializeClassLevel=3可以设置 Dubbo 在自动信任类时的 Package 层级。

举个例子,你的 Service Class 路径是org.example.echo.service.EchoService,配置TrustSerializeClassLevel=3意味着 Dubbo 会自动信任org.example.echo包下的所有类。TrustSerializeClassLevel 的默认值就是3。

properties 复制代码
dubbo.application.auto-trust-serialize-class=true
dubbo.application.trust-serialize-class-level=3

源码分析

最后,从 Dubbo 源码层面分析下实现原理。

流程图如下,实现并不复杂。

如下图所示,Service 在暴露和引用的时候,默认会注册 Service Class,方法是SerializeSecurityConfigurator#registerInterface

注册接口就是将 Service Class 自身、以及超类、属性类、方法的入参/出参、返回类型、异常类型等通通加入到信任白名单。当然,前提是开启了 autoTrustSerializeClass。

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);
        }
    }
}

addToAllow 除了会添加 Class 本身,还会根据 trustSerializeClassLevel 配置自动信任 Class Package 对应的层级。

java 复制代码
private void addToAllow(String className) {
    // ignore jdk
    if (className.startsWith("java.")
            || className.startsWith("javax.")
            || className.startsWith("com.sun.")
            || className.startsWith("sun.")
            || className.startsWith("jdk.")) {
        serializeSecurityManager.addToAllowed(className);
        return;
    }

    // add group package
    String[] subs = className.split("\\.");
    if (subs.length > trustSerializeClassLevel) {
        serializeSecurityManager.addToAllowed(
                Arrays.stream(subs).limit(trustSerializeClassLevel).collect(Collectors.joining(".")) + ".");
    } else {
        serializeSecurityManager.addToAllowed(className);
    }
}

而对于 Java 内置的基本数据类型和包装类型,以及一些其它常用且安全的类,Dubbo 源码在dubbo-common模块已经自动帮我们配置了,如下所示:

尾巴

RPC 框架离不开对象的序列化与反序列化,序列化往往又伴随着 RCE 攻击风险,因此 Dubbo 不得不开启序列化类型的检查机制,只有在白名单里的受信任的 Class 才能序列化。同时,为了不给开发者带来额外的配置负担,Dubbo 内置了"自动信任"机制,通过 Service 在暴露和引用时自动信任相关的类,既没有额外负担,又更安全。

相关推荐
呼啦啦啦啦啦啦啦啦7 小时前
常见的排序算法
java·算法·排序算法
anlogic7 小时前
Java基础 8.18
java·开发语言
练习时长一年8 小时前
AopAutoConfiguration源码阅读
java·spring boot·intellij-idea
源码宝9 小时前
【智慧工地源码】智慧工地云平台系统,涵盖安全、质量、环境、人员和设备五大管理模块,实现实时监控、智能预警和数据分析。
java·大数据·spring cloud·数据分析·源码·智慧工地·云平台
David爱编程10 小时前
面试必问!线程生命周期与状态转换详解
java·后端
LKAI.11 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi
HeyZoeHey11 小时前
Mybatis执行sql流程(一)
java·sql·mybatis
2301_7930868711 小时前
SpringCloud 07 微服务网关
java·spring cloud·微服务
柳贯一(逆流河版)12 小时前
Spring 三级缓存:破解循环依赖的底层密码
java·spring·缓存·bean的循环依赖
该用户已不存在14 小时前
OpenJDK、Temurin、GraalVM...到底该装哪个?
java·后端