FastJSON autoType is not support问题解决

概述

产品在使用内部的后台管理系统时反馈的问题。

于是登录平台,发现如下报错详情:
在这里插入图片描述

排查

经过分析,不难得知,请求是从gateway网关转发到对应的统计服务 statistics,此服务有个接口/api/statistics/data/overview。去ELK查找 statistics 服务的报错日志,没有发现ERROR日志。那就去 gateway 网关服务找,发现如下报错日志:

ERROR | o.s.b.a.web.reactive.error.AbstractErrorWebExceptionHandler | error | 122 | - [96c03c98] 500 Server Error for HTTP POST "/api/statistics/data/overview"
com.alibaba.fastjson.JSONException: autoType is not support. com.aba.rbac.modules.security.security.JwtUser
    at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:1542)
    at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:343)
    at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1430)
    at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1390)
    at com.alibaba.fastjson.JSON.parse(JSON.java:181)
    at com.alibaba.fastjson.JSON.parse(JSON.java:191)
    at com.alibaba.fastjson.JSON.parse(JSON.java:147)
    at com.alibaba.fastjson.JSON.parseObject(JSON.java:252)
    at com.aba.gateway.filter.PermissionFilter.filter(PermissionFilter.java:123)

代码解析

报错的代码如下:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 后台操作权限过滤器
 **/
@Slf4j
@Component
public class PermissionFilter implements GlobalFilter, Ordered {
    
    

    @Autowired
    private RedisTemplate redisTemplate;
    @Resource
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.header}")
    private String tokenHeader;
    @Value("${gwb.referer}")
    private String imsHost;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String requestPath = request.getURI().getPath();
        HttpHeaders headers = request.getHeaders();
        // 初始值,默认为false,表示无权限
        AtomicBoolean isPermission = new AtomicBoolean(false);
        String username = headers.getFirst("username");
        String origin = headers.getFirst("origin");
        if (!StringUtils.isEmpty(origin)) {
    
    
            if (!origin.equals(imsHost) && !origin.contains("localhost")) {
    
    
                log.error("origin非法:{}", origin);
                throw new IllegalArgumentException("origin非法!");
            }
        }
		// 排除登录接口
        if (!requestPath.contains("/auth/login/ldap") && !requestPath.contains("/api/rbac")) {
    
    
            Assert.notNull(username, "header中的username不能为空");
            final String requestHeader = headers.getFirst(this.tokenHeader);
            Boolean invalid;
            if (StringUtils.isEmpty(requestHeader)) {
    
    
                log.error("token为空!");
                invalid = true;
            } else {
    
    
                try {
    
    
                    String authToken = requestHeader.substring(7);
                    invalid = jwtTokenUtil.isTokenExpired(authToken);
                    String tokenName = jwtTokenUtil.getUsernameFromToken(authToken);
                    if (!username.equals(tokenName)) {
    
    
                        Response<Void> response = Response.error(9642, "token非法!");
                        log.info("token中用户与username不一致!");
                        // 设置body
                        DataBuffer bodyDataBuffer = response.bufferFactory().wrap(JsonUtil.beanToJson(response).getBytes(StandardCharsets.UTF_8));
                        return response.writeWith(Mono.just(bodyDataBuffer));
                    }
                } catch (Exception e) {
    
    
                    log.error("jwt校验发生异常!", e);
                    invalid = true;
                }
            }
            if (invalid) {
    
    
                Response<Void> response = Response.error(9642, "token已失效!");
                log.info("token失效!");
                //设置body
                DataBuffer bodyDataBuffer = response.bufferFactory().wrap(JsonUtil.beanToJson(response).getBytes(StandardCharsets.UTF_8));
                return response.writeWith(Mono.just(bodyDataBuffer));
            }
            ValueOperations<String, Object> operations = redisTemplate.opsForValue();
            String postData = (String) operations.get(username);
            HashSet<String> roles;
            if (StringUtils.isBlank(postData)) {
    
    
                roles = Sets.newHashSet();
            } else {
    
    
            	// 报错行
                JSONObject jsonObject = JSON.parseObject(postData);
                roles = (HashSet<String>) jsonObject.get("roles");
            }
            if (roles.contains(requestPath)) {
    
    
                isPermission.set(true);
            } else {
    
    
                roles.forEach(role -> {
    
    
                    if (requestPath.contains(role)) {
    
    
                        isPermission.set(true);
                    }
                });
            }
            // 停止转发没有用户登录的请求
            if (!isPermission.get()) {
    
    
                Response<Void> response = Response.error(9641, "权限不足,请检查配置!");
                DataBuffer bodyDataBuffer = response.bufferFactory().wrap(JsonUtil.beanToJson(response).getBytes(StandardCharsets.UTF_8));
                return response.writeWith(Mono.just(bodyDataBuffer));
            }
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
    
    
        return Integer.MIN_VALUE;
    }
}

代码解析:
检查前端请求的header=origin是否合法,检查header=username是否为空,解析header里的token是否为空,是否过期,从token里解析得到的username与header=username是否一致。全部检查通过后,从Redis里获取该用户username所具备的权限,权限是一个set集合,表示该用户具备的全部请求URL路径。对比权限set<url>与用户请求的URL,是set集合里的元素,则放行,否则报错权限不足。

另外缓存数据是在登录时放在Redis里,也就是上面的/auth/login/ldap接口里,各种检查通过后,最后做的事情。代码略。

johnny.wong用户权限信息存入缓存中:com.aba.rbac.modules.security.security.JwtUser@32525c81

Google

与此同时,Google搜索得知。参考enable_autotype,FastJSON在1.2.24及之前的版本存在代码执行漏洞,当恶意攻击者提交一个精心构造的序列化数据到服务端时,由于FastJSON在反序列化时存在漏洞,可导致远程任意代码执行。

自1.2.25+版本,禁用部分autoType的功能,也就是@type这种指定类型的功能会被限制在一定范围内使用。反序列化对象时,需要检查是否开启autoType。没有开启就会报错。

复现

生产环境发现问题,单纯靠分析日志定位到问题,并能立马解决问题,那就不叫什么问题。

如果可以在本地开发环境复现问题,那就不是什么大问题。

在上面提到的报错行打个调试断点,如期出现报错,那就好办:
在这里插入图片描述
报错出现在JSON反序列化那一行:JSONObject jsonObject = JSON.parseObject(postData);。postData是Redis里面的缓存数据。来看一下Redis里的数据长什么样:
在这里插入图片描述
发现这里面的@type和报错提到的全路径名一模一样。也就是说,我现在遇到的问题,95%概率就是上面Google搜索到的反序列化漏洞场景问题。

此时再来看看 gateway 网关服务最近的更改:
在这里插入图片描述
移除gateway网关服务里pom文件指定的版本号,直接使用parentpom文件里指定的版本号:
在这里插入图片描述
即,从1.2.20升级到1.2.83。

之前gateway网关服务使用FastJSON 1.2.20版本时,实际上就存在反序列化漏洞。网关服务存在漏洞啊!!升级到1.2.83版本后,解决反序列化漏洞,但是需要手动开启autoType,autoType默认是关闭的,否则反序列化失败。

解决过程

基于上面的结论,加上本地可以复现问题,解决问题的思路当然简单。

全局

在肯定会执行的Spring Bean类里,增加如下static代码库,开启全局autoType:

@Configuration
public class RedisConfig {
    
    
    static {
    
    
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }
}

但是还是有报错:
在这里插入图片描述
从上面截图可知,此时的报错@type是另一个全路径包名。

稳住,不慌。

继续看官方文档,Google搜索官方GitHub issue。

附:报错的源码代码片段:

if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {
    
    
    long hash = h3;
    for (int i = 3; i < className.length(); ++i) {
    
    
        hash ^= className.charAt(i);
        hash *= fnv1a_64_magic_prime;
        if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
    
    
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
            if (clazz != null) {
    
    
                return clazz;
            }
        }
        if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
    
    
            if (Arrays.binarySearch(acceptHashCodes, fullHash) >= 0) {
    
    
                continue;
            }
            throw new JSONException("autoType is not support. " + typeName);
        }
    }
}

全路径

全局开启有问题,那就开启白名单,指定一个个包名全路径:

static {
    
    
//        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    ParserConfig.getGlobalInstance().addAccept("com.aba.rbac.modules.security.security.JwtUser");
    ParserConfig.getGlobalInstance().addAccept("org.springframework.security.core.authority.SimpleGrantedAuthority");
}

无奈还是有报错:
在这里插入图片描述
报错源码如下:

if (!autoTypeSupport) {
    
    
    long hash = h3;
    for (int i = 3; i < className.length(); ++i) {
    
    
        char c = className.charAt(i);
        hash ^= c;
        hash *= fnv1a_64_magic_prime;
        if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
    
    
            if (typeName.endsWith("Exception") || typeName.endsWith("Error")) {
    
    
                return null;
            }
            throw new JSONException("autoType is not support. " + typeName);
        }
    }
}

注:上面两次throw new JSONException("autoType is not support. " + typeName),都是在源码的方法

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
    
    
}

里面。

根据上面的代码,不难得知修复方法如下,既要开启全局autoType,同时还需要手动指定下面这个全路径包名:

static {
    
    
    // https://github.com/alibaba/fastjson/wiki/enable_autotype
    ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    ParserConfig.getGlobalInstance().addAccept("com.aba.rbac.modules.security.security.JwtUser");
    ParserConfig.getGlobalInstance().addAccept("org.springframework.security.core.authority.SimpleGrantedAuthority");
}

注:为了确保这个static代码块肯定被执行,一定要放在Spring Bean扫描到的类里面,或者直接放在Spring Boot启动类里。

参考

猜你喜欢

转载自blog.csdn.net/lonelymanontheway/article/details/130975661
今日推荐