1. 漏洞概述
Fastjson 1.2.68 至 1.2.83 在默认配置(未开启 AutoType)下,攻击者通过构造包含 @JSONType 注解的远程类名,可让 Spring Boot FatJar 的 LaunchedURLClassLoader 拉取远程字节码,随后 Fastjson 实例化 @JSONType(deserializer = ...) 中指定的反序列化器,造成远程代码执行(RCE)。
- CVE:CVE-2026-16723
- 影响版本:Fastjson
1.2.68~1.2.83 - 修复版本:Fastjson
1.2.84 - 前置条件:Spring Boot 可执行 FatJar 部署、
LaunchedURLClassLoader可解析jar:http://...类路径 - 特点:无需开启 AutoType、无需本地第三方 Gadget
2. 关键调用链
JSON.parseObject / JSON.parse
└─ DefaultJSONParser.parseObject(...)
遇到 JSON 里的 "@type":"xxx.xxx"
└─ ParserConfig.checkAutoType("xxx.xxx", ...) ← 类型检查
ASM 扫描发现类文件带 @JSONType
└─ TypeUtils.loadClass(...) ← 加载类
└─ ParserConfig.getDeserializer(clazz)
└─ ParserConfig.createJavaBeanDeserializer(...)
读取 clazz 上的 @JSONType.deserializer()
└─ deserializerClass.newInstance() ← 触发代码执行
3. 1.2.83 问题代码定位
3.1 入口:@type 解析
src/main/java/com/alibaba/fastjson/parser/DefaultJSONParser.java:343
if (!allDigits) {
clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
}
...
ObjectDeserializer deserializer = config.getDeserializer(clazz);
Object obj = deserializer.deserialze(this, clazz, fieldName);
3.2 核心 bypass:@JSONType 被当作可信标记
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:1479-1502
boolean jsonType = false;
InputStream is = null;
try {
String resource = typeName.replace('.', '/') + ".class";
if (defaultClassLoader != null) {
is = defaultClassLoader.getResourceAsStream(resource);
} else {
is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}
if (is != null) {
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
}
} catch (Exception e) {
// skip
} finally {
IOUtils.close(is);
}
if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
只要类文件上带有 @JSONType 注解,jsonType 置为 true,就会绕过 AutoType 关闭时的黑白名单检查,直接加载类。
@JSONType 注解检测:
src/main/java/com/alibaba/fastjson/asm/TypeCollector.java:71-74
public void visitAnnotation(String desc) {
if (JSONType.equals(desc)) {
jsonType = true;
}
}
3.3 类加载:交给当前 ClassLoader
src/main/java/com/alibaba/fastjson/util/TypeUtils.java:1735-1793
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
...
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
...
}
} catch (Throwable e) { ... }
try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null && contextClassLoader != classLoader) {
clazz = contextClassLoader.loadClass(className);
...
}
} catch (Throwable e) { ... }
...
}
Fastjson 把类名直接交给当前 ClassLoader。在 Spring Boot FatJar 中就是 LaunchedURLClassLoader,它支持 jar: URL,从而可以拉取远程字节码。
3.4 触发 RCE:@JSONType(deserializer = ...) 被实例化
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:935-951
public ObjectDeserializer createJavaBeanDeserializer(Class<?> clazz, Type type) {
boolean asmEnable = this.asmEnable & !this.fieldBased;
if (asmEnable) {
JSONType jsonType = TypeUtils.getAnnotation(clazz, JSONType.class);
if (jsonType != null) {
Class<?> deserializerClass = jsonType.deserializer();
if (deserializerClass != Void.class) {
try {
Object deseralizer = deserializerClass.newInstance();
if (deseralizer instanceof ObjectDeserializer) {
return (ObjectDeserializer) deseralizer;
}
} catch (Throwable e) {
// skip
}
}
...
}
}
...
}
只要加载的类带有 @JSONType(deserializer = Evil.class),deserializerClass.newInstance() 就会触发 Evil 类的静态初始化块 / 构造方法 / deserialze(...),造成 RCE。
3.5 其他可利用分支
mappingTo()重定向:ParserConfig.java:628-635seeAlso()递归加载:JavaBeanDeserializer.java:1629-1652
4. 1.2.84 修复代码分析
1.2.84 没有移除 @JSONType 实例化逻辑,而是切断了“远程类名传入并被加载”这条路径,同时加固了白名单校验。
4.1 拒绝 URL 特殊字符的类名
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:1363-1367
// a URL-special type name can never be a class name; reject it here the same way any
// unresolvable @type is rejected, so it never reaches the class loader
if (TypeUtils.hasIllegalTypeNameChars(typeName)) {
throw new JSONException("autoType is not support. " + typeName);
}
src/main/java/com/alibaba/fastjson/util/TypeUtils.java:1743-1745
public static boolean hasIllegalTypeNameChars(String typeName) {
return typeName.indexOf(':') >= 0 || typeName.indexOf('!') >= 0;
}
src/main/java/com/alibaba/fastjson/util/TypeUtils.java:1779-1781
if (hasIllegalTypeNameChars(className)) {
return null;
}
一旦类名里出现 : 或 !(如 jar:http://evil/x.jar!/Payload),直接拒绝,不会交给任何 ClassLoader。
4.2 白名单哈希 + 真实名字双校验
新增 acceptNameSet:
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:146-151
/**
* The accept names behind {@link #acceptHashCodes}, normalized the same way the rolling hash in
* {@link #checkAutoType} normalizes a type name. A hash match is only honored when the matched
* prefix text is in this set, so a hash collision alone cannot whitelist a type name.
*/
private volatile Set<String> acceptNameSet = Collections.emptySet();
初始化时同步填充:
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:335-345
long[] hashCodes = new long[AUTO_TYPE_ACCEPT_LIST.length];
Set<String> acceptNames = new HashSet<String>(AUTO_TYPE_ACCEPT_LIST.length);
for (int i = 0; i < AUTO_TYPE_ACCEPT_LIST.length; i++) {
String acceptName = TypeUtils.normalizeAcceptName(AUTO_TYPE_ACCEPT_LIST[i]);
hashCodes[i] = TypeUtils.fnv1a_64(acceptName);
acceptNames.add(acceptName);
}
Arrays.sort(hashCodes);
acceptHashCodes = hashCodes;
acceptNameSet = Collections.unmodifiableSet(acceptNames);
addAccept 同步更新:
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:1298-1308
String acceptName = TypeUtils.normalizeAcceptName(name);
if (!this.acceptNameSet.contains(acceptName)) {
Set<String> names = new HashSet<String>(this.acceptNameSet);
names.add(acceptName);
this.acceptNameSet = Collections.unmodifiableSet(names);
}
long hash = TypeUtils.fnv1a_64(acceptName);
使用时的校验:
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:1429-1432
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (!acceptNameSet.contains(TypeUtils.normalizeAcceptName(className.substring(0, i + 1)))) {
continue;
}
哈希撞上了也要在真实名字集合里存在,防止碰撞绕过。
4.3 嵌套类名统一归一化
src/main/java/com/alibaba/fastjson/util/TypeUtils.java:1755-1757
public static String normalizeAcceptName(String typeName) {
return typeName.indexOf('$') >= 0 ? typeName.replace('$', '.') : typeName;
}
把 $ 统一成 .,让 A$B 和 A.B 两种写法在白名单中一致。
4.4 白名单前缀不再覆盖 Gadget 基类
src/main/java/com/alibaba/fastjson/util/TypeUtils.java:1768-1772
public static boolean isAutoTypeDenyClass(Class<?> type) {
return ClassLoader.class.isAssignableFrom(type)
|| javax.sql.DataSource.class.isAssignableFrom(type)
|| javax.sql.RowSet.class.isAssignableFrom(type);
}
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:1435-1441
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, i + 1 == className.length());
if (clazz != null) {
// matching an accept prefix is not an opt-in for gadget base types...
if (i + 1 < className.length() && TypeUtils.isAutoTypeDenyClass(clazz)) {
continue;
}
return clazz;
}
如果白名单只是前缀匹配,且匹配到 ClassLoader / DataSource / RowSet 基类,继续扫描;只有完整类名在白名单里才放行。
4.5 不再缓存被拒绝的类
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:1548-1553
if (autoTypeSupport || jsonType || expectClassFlag) {
// never cache here: a deny class (ClassLoader/DataSource/RowSet) must not be served
// from the class mapping cache on a later call before the blacklist check below;
// the accepted paths all call TypeUtils.addMapping explicitly
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
@JSONType 路径不再缓存类,避免危险类先污染缓存、后续绕过黑名单检查。
5. 1.2.83 vs 1.2.84 关键差异
| 维度 | 1.2.83 | 1.2.84 |
|---|---|---|
| 远程类名 | jar:http://...!/Payload 可传到 ClassLoader |
类名含 : 或 ! 直接拒绝 |
| 白名单校验 | 仅 FNV 哈希匹配 | 哈希 + acceptNameSet 真实名字双校验 |
| 嵌套类白名单 | A$B 与 A.B 哈希不一致 |
normalizeAcceptName 统一归一化 |
| Gadget 基类 | 白名单前缀可能覆盖 ClassLoader / DataSource / RowSet |
前缀匹配到这些类必须完整名才放行 |
| 类缓存 | @JSONType 路径会缓存 |
@JSONType 路径不缓存,仅显式 accept 才缓存 |
@JSONType.deserializer() 实例化 |
仍存在 | 仍存在,但远程类已无法被加载 |
| 版本号 | 1.2.83 |
1.2.84 |
| 构建变更 | 无 central-publishing 插件 | 新增 central-publishing-maven-plugin,恢复 JUnit 测试依赖 |
6. 修复思路总结
1.2.84 的核心思路是:在不改动 @JSONType 注解信任模型的前提下,封掉“远程类名传入并被加载”的入口。
- 输入层:禁止 URL 特殊字符,让
jar:http://...进不来。 - 校验层:哈希匹配后必须核对真实名字,防止碰撞和错误前缀。
- 加载层:危险 Gadget 基类不能通过前缀加载;被拒绝的类不再污染缓存。
7. 建议
- 短期:将 Fastjson 升级到
1.2.84或更高版本。 - 加固:开启 SafeMode(
-Dfastjson.parser.safeMode=true),或换用com.alibaba:fastjson:1.2.83_noneautotype。 - 网络层:限制应用服务器对外 HTTP 出站,防止拉取远程 jar。
- 长期:迁移到 Fastjson2,1.x 已进入维护模式。
8. 相关文件路径
- 漏洞根因:
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java - 类加载:
src/main/java/com/alibaba/fastjson/util/TypeUtils.java - 注解扫描:
src/main/java/com/alibaba/fastjson/asm/TypeCollector.java - 反序列化器实例化:
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java - 回归测试:
src/test/java/com/alibaba/json/bvt/parser/autoType/AutoTypeValidationTest.java(1.2.84 新增)
本地仓库:
/test(tag1.2.83)
修复版本:1.2.84
分析方式:git diff 1.2.83..1.2.84