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