2021SC@SDUSC
使用Fastjson进行反序列化时,我们总希望能够对反序列化的过程进行一个定制,有时候这种定制我们希望是临时的,有时希望是全局的。之前提到过我们可以通过feature参数对反序列化的属性进行定制,包括是否允许使用大括号、是否允许多重逗号等。今天谈一下ParserConfig类,它拥有比feature更强大的功能,从更高的角度对反序列化过程进行控制,支持全局定制,也可以进对某一次反序列化改变其默认的行为。下面开始分析。
目录
- ParserConfig的使用
- ParserConfig的变量及含义
- ParserConfig的关键方法解析
- checkAutoType(String typeName, Class<?> expectClass, int features)
- public void configFromPropety(Properties properties)
- 总结
ParserConfig的使用
上一次测试ASM的代码中,已经用到了ParserConfig的全局对象,进行ASM enable的设置。这次我们尝试使用局部对象来定制反序列化。
@Test
public void test03(){
String str="{\"age\":13,\"name\":\"james\"}";
ParserConfig config=new ParserConfig();
config.setAsmEnable(false);
Student student= JSON.parseObject(str,Student.class,config);
System.out.println("age:"+student.getAge());
System.out.println("name:"+student.getName());
}
这里我们使用config对象,设置了ASM为关闭状态(默认打开),然后对json字符串进行了反序列化。这就是该类的简单用法。
ParserConfig的变量及含义
public static final String DENY_PROPERTY = "fastjson.parser.deny";
public static final String AUTOTYPE_ACCEPT = "fastjson.parser.autoTypeAccept";
public static final String AUTOTYPE_SUPPORT_PROPERTY = "fastjson.parser.autoTypeSupport";
public static final String[] DENYS;
private static final String[] AUTO_TYPE_ACCEPT_LIST;
public static final boolean AUTO_SUPPORT;
public static ParserConfig global;
private final IdentityHashMap<Type, ObjectDeserializer> deserializers;
private boolean asmEnable;
public final SymbolTable symbolTable;
public PropertyNamingStrategy propertyNamingStrategy;
protected ClassLoader defaultClassLoader;
protected ASMDeserializerFactory asmFactory;
private static boolean awtError;
private static boolean jdk8Error;
private boolean autoTypeSupport;
private long[] denyHashCodes;
private long[] acceptHashCodes;
public final boolean fieldBased;
public boolean compatibleWithJavaBean;
解释一下这里面最关键的变量:AUTO_SUPPORT
:表示自动类型反序列化是否打开。打开后,允许用户在反序列化数据中通过“@type”指定反序列化的Class类型。
global
:全局ParserConfig对象,对整个项目进行控制,包括是否使用asm、是否打开autotype等
asmEnable
:设置asm是否可用,默认在安卓环境下为false(处于性能考虑),其他环境下为true
defaultClassLoader
:默认的类加载器
ParserConfig的关键方法解析
checkAutoType(String typeName, Class<?> expectClass, int features)
fastjson需要将json字符串反序列化成对象,就需要调用对象的getter和setter,如果这些方法里面存在危险操作,就会导致漏洞。意识到这一问题,开发者使用了checkAutoType方法来检测类是否允许被反序列化。简单来说,这个方法就是一个黑名单检测方法,下面看一下源代码。
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() < 128 && typeName.length() >= 3) {
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
long h1 = (-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L;
if (h1 == -5808493101479473382L) {
throw new JSONException("autoType is not support. " + typeName);
} else if ((h1 ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
throw new JSONException("autoType is not support. " + typeName);
} else {
long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
long hash;
int i;
if (this.autoTypeSupport || expectClass != null) {
hash = h3;
for(i = 3; i < className.length(); ++i) {
hash ^= (long)className.charAt(i);
hash *= 1099511628211L;
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
if (!this.autoTypeSupport) {
hash = h3;
for(i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= (long)c;
hash *= 1099511628211L;
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}
if (clazz != null) {
if (TypeUtils.getAnnotation(clazz, JSONType.class) != null) {
return clazz;
}
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, this.propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && this.autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport || (features & mask) != 0 || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
} else {
return clazz;
}
}
}
} else {
throw new JSONException("autoType is not support. " + typeName);
}
}
代码很长,总结一下该类的处理顺序:
- 之前黑名单绕过的修复
- 开启 autoTypeSupport 或者 expectClass 不为空时的黑白名单(白名单直接返回)
- 尝试从缓存 Map 中加载类,若成功且无 expectClass 时可以直接返回
- 尝试从 deserializers 中加载类,若成功且无 expectClass 时可以直接返回
- 未开启 autoTypeSupport 时的黑白名单
- 加载类
- expectClass 校验和注解校验,如果通过则返回
- 未开启 autoTypeSupport 则抛出异常
这种黑名单检测机制,保证了黑客无法通过写恶意类来绕过autotyoe的反序列化过程对程序造成坏的影响。
public void configFromPropety(Properties properties)
这个方法如其名字一样,从配置类导入配置,完成对ParserConfig的初始化。
public void configFromPropety(Properties properties) {
String property = properties.getProperty("fastjson.parser.deny");
String[] items = splitItemsFormProperty(property);
this.addItemsToDeny(items);
property = properties.getProperty("fastjson.parser.autoTypeAccept");
items = splitItemsFormProperty(property);
this.addItemsToAccept(items);
property = properties.getProperty("fastjson.parser.autoTypeSupport");
if ("true".equals(property)) {
this.autoTypeSupport = true;
} else if ("false".equals(property)) {
this.autoTypeSupport = false;
}
}
可以看到,该方法首先从配置类中获取fastjson.parser.deny
值,保存为字符串,然后分隔该字符串得到每个deny值,再保存在字符串数组里面,最终增添到ParserConfig对象中。然后获取fastjson.parser.autoTypeAccept
值,以同样的方法增添到ParserConfig对象中。最后,获取fastjson.parser.autoTypeSupport
,设置对应的值。这里解释一下这三个变量:deny为拒绝反序列化的类,即黑名单,禁止这些类反序列化。accept反之。其中accept优先级高于deny。autotypeSupport表示是否支持自动类型转换。
总结
本篇提到了ParserConfig类既可以对项目进行全局的控制,也可以用来生成临时变量,将某一次或几次的反序列化改变行为。ParserConfig中涉及许多方法,还包括了黑名单、白名单的存在,用来防治恶意攻击。
下一篇我们继续深入ParserConfig类,探究它更深层次的用法。