fastjson 1.2.84 的补丁关掉了一个缓存:addAccept 包前缀白名单下,反序列化慢了 13~28 倍

最近后台总有人搜「fastjson 1.2.84 解析报错」「fastjson 1.2.84 和 1.2.83 区别」。1.2.84 是只修安全问题的小版本,照理说直接换版本号就行。我把 1.2.83 和 1.2.84 放在一起,对同一批写法逐条跑了一遍,结果有两处要注意。

先说结论

  • 大部分写法不受影响 :普通对象、TypeReference 泛型、不带白名单的 @type(两版都拒)、SafeMode、全开 autoType 往返、字段声明父类 + @type 子类,两版结果完全一样。
  • 会报错的一种 :用 addAccept("com.xxx.") 这种包前缀 白名单,反序列化一个实现了 javax.sql.DataSource 的类 (比如继承 Spring AbstractRoutingDataSource 的动态数据源)。1.2.83 正常,1.2.84 报 autoType is not support。
  • 会变慢的两种 :同样是包前缀白名单,以及不开 autoType 时加了 @JSONType 的类。带 @type 的反序列化,JDK 8 上慢约 13 倍和 23 倍,JDK 17 上两种都慢约 28 倍。
  • 一个办法解决两个问题 :把白名单从包前缀改成完整类名。实测报错消失,耗时也回到和 1.2.83 一样。
  • 官方发布说明里没有提这两处变化。

官方改了什么

1.2.83 到 1.2.84 一共 4 个提交,主代码只动了两个文件:

text 复制代码
modified  +56/-6   src/main/java/com/alibaba/fastjson/parser/ParserConfig.java
modified  +43/-0   src/main/java/com/alibaba/fastjson/util/TypeUtils.java
modified  +20/-1   pom.xml
added     +147/-0  src/test/java/.../autoType/AutoTypeValidationTest.java

发布说明列了 4 条修复:拒绝类型名里带 : / !;白名单 hash 命中后再比一次原文;包前缀白名单不再放行 ClassLoader / DataSource / RowSet 这类危险基类;危险类不再在黑名单检查之前被放进缓存。

这次要修的漏洞是 CVE-2026-16723,版本该怎么看,前一篇已经按版本轴梳理过:fastjson 1.2.49 中了 CVE-2026-16723 吗?1.2.84 和 1.2.83 到底差在哪。还在 JDK 7 上的项目,1.2.84 根本加载不起来,另一篇有实测:fastjson 1.2.84 能在 JDK 7 上跑吗?。本文只讲 JDK 8 及以上升级后行为有没有变。

实测方法

  • jar 直接从 Maven Central 下载,SHA1 与 Central 上的 .sha1 一致:1.2.83 9ee94951bc107d382519975d04bc950b6c6ab297,1.2.84 05773609a8ce27cc9cbfbec0009e02b1a7b83dde。
  • 同一份测试代码分别对两个 jar 编译、运行。fastjson 的类缓存是全局静态的,混在一个进程里跑会互相污染,所以每个用例单独起一个 JVM。
  • JDK:Zulu 1.8.0_504、Oracle 17.0.4.1。

功能对照:17 种写法,4 种结果不同(归结起来是 3 处变化)

写法 1.2.83 1.2.84
JSON.VERSION 1.2.83 1.2.83(没改,见第 34 注那篇)
普通对象 parseObject(json, User.class) + 序列化 ✅ ✅
TypeReference<List<User>> 泛型 ✅ ✅
带 @type、默认配置(没开 autoType、没白名单) ❌ autoType is not support ❌ 同左
带 @type + addAccept("demo.") 包前缀 ✅ ✅
白名单写内部类的二进制名 addAccept("demo.Outer$Inner") ❌ autoType is not support ✅ 1.2.84 修好了
白名单写内部类的点号名 addAccept("demo.Outer.Inner") ✅ ✅
包前缀白名单 + 实现了 DataSource 的类(不开 autoType) ✅ ❌ autoType is not support. demo.MyDataSource
包前缀白名单 + 实现了 DataSource 的类(开 autoType) ✅ ❌ 同上
完整类名白名单 addAccept("demo.MyDataSource") ✅ ✅
只开 autoType、不配白名单,反序列化 DataSource 类 ❌ ❌ 同左
DataSource 类作为普通字段类型(不靠 @type) ✅ ✅
全开 autoType,WriteClassName 序列化再读回 ✅ ✅
@JSONType 注解类 + @type(不开 autoType) ✅ ✅
SafeMode + 白名单 ❌ safeMode not support autoType ❌ 同左
类型名带冒号 demo:User(开 autoType) ❌ ClassCastException(报错很怪) ❌ autoType is not support. demo:User
字段声明父类 Animal,JSON 里 @type 写子类 Dog ✅ ✅

另外用真实的 Spring 动态数据源测了一次(spring-jdbc 5.3.39,一个继承 AbstractRoutingDataSource 的类,不开 autoType):

白名单写法 1.2.83 1.2.84
addAccept("demo.") ✅ ❌ autoType is not support. demo.DynDs
addAccept("demo.DynDs") ✅ ✅

那条报错为什么不好查

1.2.84 拒绝这类类时,报的是 autoType is not support. 类名,和「没配白名单」时的报错一字不差 。白名单明明写了 com.xxx.,报错却像白名单没生效,很容易往「配置没加载上」的方向查。

原因在 ParserConfig.checkAutoType 新加的这几行(1.2.84 源码第 1436~1444 行):

java 复制代码
if (clazz != null) {
    // matching an accept prefix is not an opt-in for gadget base types, only an
    // accept entry naming the type in full is; keep scanning for such an entry
    if (i + 1 < className.length() && TypeUtils.isAutoTypeDenyClass(clazz)) {
        continue;
    }
    return clazz;
}

isAutoTypeDenyClass 判断的是 ClassLoader / javax.sql.DataSource / javax.sql.RowSet 的子类。也就是说,只有白名单写完整类名,才算你明确放行了这个类,包前缀不算。这是安全上有意的收紧,发布说明第 3 条说的就是它。

性能对照:两种写法慢了一个数量级

同一条 JSON 反序列化 100 万次,先热身 20 万次,取每次平均耗时。每组单独起 JVM,跑 3 轮取中位数,单位纳秒(ns)。这是单机微基准,看倍数,别看绝对值。

写法 JDK 8:1.2.83 JDK 8:1.2.84 JDK 17:1.2.83 JDK 17:1.2.84
普通对象(不带 @type) 46 44 73 78
完整类名白名单 + @type 121 127 155 163
全开 autoType + @type 159 185 206 206
包前缀白名单 + @type 124 1,658(约 13 倍) 153 4,326(约 28 倍)
@JSONType 类 + @type(不开 autoType) 105 2,396(约 23 倍) 159 4,518(约 28 倍)
开 autoType 同时 配包前缀白名单 + @type 138 421(约 3 倍) 150 285(约 2 倍)

三轮原始数据(1.2.84,包前缀白名单):JDK 8 是 1,642 / 1,658 / 1,687,JDK 17 是 4,326 / 3,740 / 4,535,各轮之间很稳定。

按 JDK 17 的数字算,一个响应里有 1,000 个带 @type 的对象,每次反序列化就多出约 4 毫秒。

慢在哪

用 JFR 在 JDK 17 上采样 1.2.84 的这两种写法,93%~94% 的采样都落在同一个方法上:com.alibaba.fastjson.util.IdentityHashMap.findClass。

这个方法会把 ParserConfig 里反序列化器表的每一个桶都扫一遍,桶数默认是 8192(IdentityHashMap.DEFAULT_SIZE = 8192):

java 复制代码
public Class findClass(String keyString) {
    for (int i = 0; i < buckets.length; i++) {
        ...
            if (className.equals(keyString)) {
                return clazz;
            }
    }
    return null;
}

1.2.83 走不到这里,原因是类在第一次之后就进了全局缓存:

java 复制代码
// 1.2.83:前缀白名单命中后加载类,第三个参数 true = 放进缓存
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);

1.2.84 改成了只有完整类名命中才缓存:

java 复制代码
// 1.2.84:i + 1 == className.length() 只在完整类名命中时为 true
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, i + 1 == className.length());

@JSONType 那条路也一样:1.2.83 在 autoTypeSupport || jsonType 时缓存;1.2.84 改成 loadClass(typeName, defaultClassLoader, false),之后只在开了 autoType 时才调用 addMapping。

没进缓存,下一次 checkAutoType 先查缓存(TypeUtils.getClassFromMapping)落空,接着就调 deserializers.findClass(typeName),每次都要把 8192 个桶扫一遍。

源码注释写明了不缓存是故意的(「a prefix match ... must not populate the class mapping cache」),为的是不让危险类从缓存里绕过黑名单。所以这是安全收紧带来的性能代价,不是写错了。发布说明里没有提这一点。

怎么确认就是缓存的问题

做了两组对照:

写法 JDK 8:1.2.84 JDK 17:1.2.84
包前缀白名单,另外手动 TypeUtils.addMapping("demo.User", User.class) 114(恢复) 164(恢复)
@JSONType 类,另外 addAccept("demo.Tagged") 完整类名 92(恢复) 152(恢复)

把类放回缓存,耗时就和 1.2.83 一样了。第一组只是为了验证原因,不建议在业务里这么写 :按源码,checkAutoType 会先查这个缓存,命中就直接返回,白名单检查就被跳过了。

升级前怎么自查

在代码里搜这几样:

bash 复制代码
grep -rn --include=*.java -e "addAccept" -e "@JSONType" -e "AutoTypeSupport" src/
grep -rn --include=*.java -e "implements DataSource" -e "AbstractRoutingDataSource" src/
grep -rn "fastjson.parser.autoTypeAccept" .   # 配置文件或启动参数 -D 里写的白名单,走的也是 addAccept

对照下面这张表:

你的情况 升 1.2.84 后 怎么办
不用 @type,只是普通的 parseObject / toJSONString 没变化 直接升
开了 SafeMode 没变化 直接升
全开 autoType、没配白名单(比如 fastjson 自带的 GenericFastJsonRedisSerializer,它内部就是 setAutoTypeSupport(true)) 功能没变化;JDK 8 上耗时中位数 159 → 185 ns,JDK 17 上持平 直接升。全开 autoType 本身就是这次漏洞要防的场景,能关最好关
addAccept("com.xxx.") 包前缀白名单 带 @type 的反序列化慢一个数量级;其中 DataSource 子类直接报错 改成完整类名,一个类一行
@JSONType 注解类,不开 autoType,靠 @type 反序列化 慢一个数量级 把这些类也用完整类名 addAccept 一遍

改法示例:

java 复制代码
ParserConfig config = ParserConfig.getGlobalInstance();
// 1.2.84 下会慢,DataSource 子类会被拒
// config.addAccept("com.example.");

// 改成完整类名
config.addAccept("com.example.order.OrderDTO");
config.addAccept("com.example.user.UserDTO");
config.addAccept("com.example.ds.DynamicDataSource");

内部类写 Outer$Inner 或 Outer.Inner 都行(1.2.84 把两种写法统一了;1.2.83 只认点号写法)。

复现代码

完整代码就两个类,核心部分如下:

java 复制代码
// 功能对照:每个用例单独起 JVM 跑
ParserConfig cfg = new ParserConfig();
cfg.addAccept("demo.");
Object o = JSON.parseObject("{\"@type\":\"demo.MyDataSource\",\"url\":\"x\"}", Object.class, cfg);
// 1.2.83: demo.MyDataSource
// 1.2.84: JSONException: autoType is not support. demo.MyDataSource
java 复制代码
// 性能对照:热身 20 万次,再计时 100 万次
ParserConfig cfg = new ParserConfig();
cfg.addAccept("demo.");
String json = "{\"@type\":\"demo.User\",\"id\":1,\"name\":\"tom\"}";
for (int i = 0; i < 200_000; i++) JSON.parseObject(json, Object.class, cfg);
long t0 = System.nanoTime();
for (int i = 0; i < 1_000_000; i++) JSON.parseObject(json, Object.class, cfg);
System.out.println((System.nanoTime() - t0) / 1_000_000 + " ns/次");
bash 复制代码
javac -cp fastjson-1.2.84.jar -d out src/demo/*.java
java -cp "fastjson-1.2.84.jar;out" demo.Bench accept_prefix   # Linux 下分号换冒号

demo.MyDataSource 就是一个实现了 javax.sql.DataSource、多一个 url 属性的普通类,方法全是空实现。

数据来源(均为 2026-10-14 读取 / 实跑)

  • 1.2.83 → 1.2.84 的改动:https://github.com/alibaba/fastjson/compare/1.2.83...1.2.84
  • 发布说明:https://github.com/alibaba/fastjson/releases/tag/1.2.84
  • 源码行号:tag 1.2.84 的 ParserConfig.java 与 IdentityHashMap.java
  • jar:https://repo1.maven.org/maven2/com/alibaba/fastjson/
相关推荐
崎岖Qiu1 小时前
MCP 的架构、协议分层与通信过程
后端·ai·架构·agent·mcp
Henry-SAP1 小时前
SAP PP主数据关联业务视角全解析
后端
万联WANFLOW1 小时前
LLM 调用很快,AI Agent 为什么还是慢?关键路径分析与优化方法
性能优化·llm·aigc
小羊没烦恼!2 小时前
如何在IIS6,7中部署ASP.NET网站?
数据库·后端·python·web安全·asp.net
IT_陈寒2 小时前
Vite打包的诡异行为,让我差点提交了个错误版本
前端·人工智能·后端
XIAEE2 小时前
转生到儿童编程异世界:我用 Vue 和 Scratch 建了一座 DREAM 学院
java·spring
XIAEE2 小时前
初次使用 Node + Koa + MongoDB 搭建服务器:从零到第一个 API
java·spring
zed_232 小时前
Agent 是什么:从「问答」到「会用工具」
后端
m0_587383002 小时前
无人自助拼豆系统核心功能开发与部署落地全流程实战指南
java·经验分享·后端·struts·架构