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 在暴露和引用时自动信任相关的类,既没有额外负担,又更安全。

相关推荐
fouryears_2341713 分钟前
适配器模式——以springboot为例
java·spring boot·适配器模式
汽车功能安全啊1 小时前
利用对称算法及非对称算法实现安全启动
java·开发语言·安全
paopaokaka_luck2 小时前
基于Spring Boot+Vue的吉他社团系统设计和实现(协同过滤算法)
java·vue.js·spring boot·后端·spring
Warren983 小时前
Java Stream流的使用
java·开发语言·windows·spring boot·后端·python·硬件工程
架构师沉默4 小时前
Java优雅使用Spring Boot+MQTT推送与订阅
java·开发语言·spring boot
tuokuac5 小时前
MyBatis 与 Spring Boot版本匹配问题
java·spring boot·mybatis
zhysunny5 小时前
05.原型模式:从影分身术到细胞分裂的编程艺术
java·原型模式
草履虫建模6 小时前
RuoYi-Vue 项目 Docker 容器化部署 + DockerHub 上传全流程
java·前端·javascript·vue.js·spring boot·docker·dockerhub
皮皮林5516 小时前
强烈建议你不要再使用Date类了!!!
java
做一位快乐的码农7 小时前
基于Spring Boot和Vue电脑维修平台整合系统的设计与实现
java·struts·spring·tomcat·电脑·maven