Fastjson 1.2.68-1.2.83 @JSONType 远程代码执行漏洞(CVE-2026-16723)代码层分析


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-635
  • seeAlso() 递归加载: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$BA.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$BA.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(tag 1.2.83
修复版本:1.2.84
分析方式:git diff 1.2.83..1.2.84

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部