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

代码很长,总结一下该类的处理顺序:

  1. 之前黑名单绕过的修复
  2. 开启 autoTypeSupport 或者 expectClass 不为空时的黑白名单(白名单直接返回)
  3. 尝试从缓存 Map 中加载类,若成功且无 expectClass 时可以直接返回
  4. 尝试从 deserializers 中加载类,若成功且无 expectClass 时可以直接返回
  5. 未开启 autoTypeSupport 时的黑白名单
  6. 加载类
  7. expectClass 校验和注解校验,如果通过则返回
  8. 未开启 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类,探究它更深层次的用法。