在一次日志巡检过程中,发现线上业务出现报错。线上业务场景是:调用三方restful接口,根据接口返回json字符串内容,进行反序列化处理,业务中使用的json处理工具是FastJson(v1.2.71)。
报错是使用fastJson进行反序列化时出现的,问题json字符串如下:
{
"name":"",
"error":{
"@type":"xxx.xxx.ErrorType",
"message":"xxxxx"
}
}
从数据结构上看,这个json字符串并没有什么特殊性,就是一个普通的字符串而已。从内容上看,比较特殊的地方在于多了一个特殊的字段“@type”,经过测试,反序列化报错,的确是因为这个特殊的字段造成的(把@type字段去掉,反序列化可以正常进行)。
那么@type是什么鬼,为什么有了字段就会产生问题呢?
使用过json序列化工具,处理包含接口、抽象类等无法直接实例化的字段类型时,你是否想过这些序列化工具是如何进行反序列化的吗?如果没有看明白这个问题的话,可以看一下下面的代码实例。
static class VehicleStore {
private String name;
private Vehicle vehicle;
// 省略 setter/getter
}
interface Vehicle {
}
static class Car implements Vehicle {
private BigDecimal price;
// 省略 setter/getter
}
上面代码定义了一个比较简单的数据类型,VehicleStore类中包含了一个类型是接口Vehicle 的字段 vehicle,接口Vehicle有一个实现类Car。
使用FastJson对VehicleStore进行序列化。
@Test
public void deserializer() {
VehicleStore store = new VehicleStore();
store.setName("vehicleStore");
Car car = new Car();
car.setPrice(new BigDecimal(5000000));
store.setVehicle(car);
String jsonString = JSON.toJSONString(store);
}
序列化后的json字符串内容如下:
{"name":"vehicleStore","vehicle":{"price":5000000}}
在这个反序列化后的字符串内容中,vehicle字段的内容是 {“price”:5000000} ,无论从内容上、还是结构上看,根本看不出 {“price”:5000000} 和类型Car,有什么关系,那么用这个json字符串,进行反序列化,如何反序列化出 VehicleStore,并且把字段vehicle指定为Car呢?
其实是不能的,如果直接用FastJson进行反序列化,反序列化出来的VehicleStore中vehicle字段是空(没有任何字段内容)。
总结来说:当一个类中包含了一个接口(或抽象类),在使用fastjson进行序列化的时候,会将子类型抹去,只保留接口(抽象类)的类型,使得反序列化时无法拿到原始类型
为了解决这个问题,在fastjson中引入了AutoType机制,简单来说,既然无法正常反序列化是序列化时把原始类型擦除掉了导致的,那么在序列化时,把类型信息给添加上可以了。的确,FastJson也是这样做的,具体做法如下:
序列化:
在进行序列化时,使用
JSON.toJSONString(xxxObj, SerializerFeature.WriteClassName);
序列化出来的内容会带上"@type"字段,记录被序列化的类型,弥补类型擦除的问题。序列化后,内容如下:
{"@type":"com.test.FastJsonTest$VehicleStore","name":"vehicleStore","vehicle":{"@type":"com.test.FastJsonTest$Car","price":5000000}}
反序列化:
反序列化时使用@type字段完成对子类/实现类的实例化,然后,再将相应的字段内容通过setter方法注入到实例化后的对象中,完成反序列化。
完整实例代码:
@Test
public void deserializer() {
VehicleStore store = new VehicleStore();
store.setName("vehicleStore");
Car car = new Car();
car.setPrice(new BigDecimal(5000000));
store.setVehicle(car);
String jsonString = JSON.toJSONString(store, SerializerFeature.WriteClassName);
VehicleStore newStore = JSON.parseObject(jsonString, VehicleStore.class);
Car car = (Car) newStore.getVehicle();
}
到这里在文章开头,遇到的@type是怎么回事儿,也就清楚了。那么为什么带上@type后,反序列化会报错呢?这里我直接说一下答案:直接原因是@type指定的类型,在系统中不存在,在反序列化时,找不到该类型。
解决问题的方法也很简单,既然@type指定的类型不存在,那么可以在系统中把需要的类型定义出来,不过@type指定的类型,是三方接口系统中定义的,如果把该类型引入到调用方系统中,那么对于调用方的侵入性很大,而且定义这个类型,仅仅为了解决这个问题,有很大解释成本在里面。
第二种解决方案:在反序列化时,忽略@type字段,这需要在反序列化时,添加Feature.IgnoreAutoType,完整的配置方式如下:
VehicleStore newStore1 = JSON.parseObject(jsonStr, VehicleStore.class, Feature.IgnoreAutoType);
到这里,开头的问题已经得到了解决,但是了解了AutoType机制后,你是否想过,如果把@Type指定为一个比较“特殊”的类型,有可能会对系统的安全产生比较大的威胁。
比如,把@type指定一个jdk中自带的类型 com.sun.rowset.JdbcRowSetImpl ,同时把字段dataSourceName设置为一个rmi数据源,那么对rmi数据源进行解析时,就完成了对rmi的调用,实现了对系统的远程命令执行。
具体操作过程如下(此漏洞,已修复,无法复现了,哈哈哈)
public void setDataSourceName(String var1) throws SQLException {
if (this.getDataSourceName() != null) {
if (!this.getDataSourceName().equals(var1)) {
super.setDataSourceName(var1);// 注入攻击rmi源
this.conn = null;
this.ps = null;
this.rs = null;
}
} else {
super.setDataSourceName(var1);
}
}
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());// 调用rmi方法
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}
其实FastJson的很多安全漏洞都和AutoType有关,当黑客发现了一个漏洞后,作者会立即进行相应的修复,在安全的攻防过程中,fastJson对AutoType进行了多个版本的更新:
1.2.59发布,增强AutoType打开时的安全性 fastjson
1.2.60发布,增加了AutoType黑名单,修复拒绝服务安全问题 fastjson
1.2.61发布,增加AutoType安全黑名单 fastjson
1.2.62发布,增加AutoType黑名单、增强日期反序列化和JSONPath fastjson
1.2.66发布,Bug修复安全加固,并且做安全加固,补充了AutoType黑名单 fastjson
1.2.67发布,Bug修复安全加固,补充了AutoType黑名单 fastjson
1.2.68发布,支持GEOJSON,补充了AutoType黑名单。(引入一个safeMode的配置,配置safeMode后,无论白名单和黑名单,都不支持autoType。) fastjson
1.2.69发布,修复新发现高危AutoType开关绕过安全漏洞,补充了AutoType黑名单 fastjson
1.2.70发布,提升兼容性,补充了AutoType黑名单
对此过程有感兴趣的老铁可以阅读一下下面这个文章:https://juejin.cn/post/6846687594130964488