背景
Fastjson 的 AutoType 问题算是 Java Web 安全里被反复讨论的一类风险。很多团队对它的印象停留在“升级版本”或者“关闭 AutoType”,但在真实项目里,风险通常不是一个依赖版本号能完全说明的:历史兼容代码、第三方 SDK、网关透传、MQ 消息消费、缓存反序列化,都可能让 JSON 反序列化进入一个不容易被注意到的位置。
这篇文章主要整理一套偏防守和代码审计视角的方法:如何确认项目是否使用了 Fastjson,如何定位危险调用点,如何判断输入是否可控,以及如何给出相对稳妥的治理方案。文中不提供攻击利用流程,只讨论审计、验证和修复。
问题场景
我在做代码审计时,比较常见的场景有几类:
- 老项目直接使用
JSON.parseObject(String)解析 HTTP 请求体。 - 为了兼容多态对象,业务代码开启过 AutoType 或使用了带类型信息的反序列化。
- 消息队列消费者把消息体当成可信输入,直接反序列化成业务对象。
- 缓存、配置中心、任务调度参数里保存了 JSON 字符串,读取后直接解析。
- 项目本身没有显式依赖 Fastjson,但某些内部 SDK 或老版本组件传递引入了 Fastjson。
这些场景的共同点是:反序列化入口不一定来自外部 HTTP 接口,也不一定在 Controller 层。只看接口层经常会漏掉 MQ、定时任务、后台管理功能和内部 RPC。
技术分析
Fastjson 的风险核心不只是“JSON 解析”,而是“解析过程中是否允许根据输入中的类型信息实例化非预期类”。如果业务允许外部输入影响目标类型,或者开启了较宽泛的类型识别能力,就可能扩大风险面。
审计时我一般把问题拆成四个维度:
| 维度 | 关注点 | 判断方式 |
|---|---|---|
| 依赖版本 | 是否使用历史高风险版本,是否存在传递依赖 | 查看 Maven/Gradle 依赖树 |
| 调用方式 | 是否存在 parseObject、parseArray、toJavaObject 等入口 | 全局搜索调用点 |
| 输入来源 | 输入是否来自 HTTP、MQ、RPC、缓存、配置中心 | 结合调用链和数据流分析 |
| 类型控制 | 是否允许输入影响反序列化类型 | 查看 Feature、ParserConfig、目标 Class 传入方式 |
关键步骤
下面是一套比较实用的排查流程,适合在代码审计或版本治理时使用。
1. 确认依赖来源
先不要只看 pom.xml,很多项目是通过公共包间接引入 Fastjson 的。建议直接看完整依赖树。
mvn dependency:tree | grep -i fastjson
Gradle 项目可以使用:
./gradlew dependencies | grep -i fastjson
如果是多模块项目,建议在根目录和关键业务模块分别执行一次。审计记录里至少保留以下信息:
- Fastjson 版本号。
- 是直接依赖还是传递依赖。
- 由哪个模块引入。
- 是否存在多个版本冲突。
2. 搜索高关注调用点
常见入口可以先用文本搜索粗筛,后续再结合 IDE 或代码审计工具做调用链分析。
grep -R "JSON.parseObject" -n src
grep -R "JSON.parseArray" -n src
grep -R "JSONObject.parseObject" -n src
grep -R "toJavaObject" -n src
grep -R "ParserConfig" -n src
grep -R "autoType" -ni src
需要注意的是,很多项目会封装一个 JsonUtil、SerializeUtil、MessageConvertor 之类的工具类,真正的危险点不一定散落在业务代码里,而是集中在这些公共方法里。审计时要把这些工具类标成重点。
3. 判断输入是否可控
不是所有 parseObject 都有同等风险。判断优先级时,可以按输入来源排序:
- 高优先级:HTTP 请求体、请求参数、Header、Cookie、Webhook 回调。
- 高优先级:MQ 消息体,尤其是跨系统、跨团队生产的 Topic。
- 中优先级:配置中心、数据库字段、Redis 缓存,取决于写入权限边界。
- 低优先级:本地静态配置、服务内部固定字符串。
这里有个经验点:内部系统的 MQ 和配置中心不要默认视为可信。很多事故里,入口并不是公网接口,而是某个低权限后台功能先写入数据,再由高权限服务异步消费。
4. 关注类型信息和宽泛反序列化
审计时重点看下面几种写法:
- 解析为
Object.class、JSONObject.class后再动态转换。 - 目标类型由外部参数决定,例如从请求字段、配置项中读取类名。
- 显式配置过 AutoType 支持。
- 使用全局 ParserConfig 修改默认行为。
- 为了兼容历史数据,允许解析带类型标记的 JSON。
相对安全的写法通常是:目标类型固定、字段白名单明确、输入来源有权限边界、解析后有业务校验。例如只解析成确定的 DTO,并且 DTO 字段都是基础类型或受控枚举。
5. 给出修复建议
治理方案要结合项目现实情况,不能只写一句“升级”。我一般会按短期、中期、长期三层给建议。
| 阶段 | 建议 | 说明 |
|---|---|---|
| 短期 | 升级到维护版本,确认不存在多版本冲突 | 先降低已知风险面 |
| 短期 | 关闭不必要的 AutoType 能力 | 避免输入影响实例化类型 |
| 中期 | 替换危险封装方法 | 对 JsonUtil 等公共入口做统一治理 |
| 中期 | 固定反序列化目标类型 | 禁止外部输入决定 Class |
| 长期 | 建立依赖准入和 SCA 检查 | 避免历史问题回流 |
参考性的安全封装思路
下面是一个偏保守的封装思路:业务代码不要直接解析成 Object,也不要从外部输入拼接目标类型,而是要求调用方传入固定 Class,并在解析后做基础校验。
public final class SafeJson {
private SafeJson() {}
public static <T> T parse(String json, Class<T> clazz) {
if (json == null || json.length() > 1024 * 1024) {
throw new IllegalArgumentException("invalid json length");
}
if (clazz == Object.class) {
throw new IllegalArgumentException("Object.class is not allowed");
}
return com.alibaba.fastjson.JSON.parseObject(json, clazz);
}
}
这段代码不是万能修复,只是表达一个原则:公共入口应该限制输入大小、限制目标类型、避免宽泛反序列化。实际项目里还需要结合 DTO 校验、字段白名单、异常处理和日志脱敏。
审计记录模板
为了让结论可复核,建议每个风险点至少记录下面这些内容:
- 文件路径和方法名。
- Fastjson 调用方式。
- 输入来源和权限边界。
- 目标反序列化类型是否固定。
- 是否依赖全局 ParserConfig 或 AutoType 配置。
- 建议修复方式和影响范围。
- 修复后验证方式。
如果团队有代码扫描流程,也可以把高风险调用点沉淀成规则。规则不一定一开始就很复杂,先把明显危险的模式拦住即可。
patterns:
- pattern: JSON.parseObject($INPUT, Object.class)
- pattern: JSONObject.parseObject($INPUT)
message: "Avoid broad Fastjson deserialization. Use fixed DTO class and validate input."
severity: WARNING
验证与回归
修复后不要只看编译通过,建议至少做三类验证:
- 功能回归:确认历史 JSON 数据仍能被正确解析。
- 边界测试:空字符串、超长 JSON、缺失字段、未知字段、类型错误。
- 安全回归:确认不再允许外部输入影响反序列化目标类型。
日志也需要注意,解析失败时不要把完整请求体直接打到日志里。很多 JSON 里会包含 Token、手机号、邮箱、内部配置等敏感信息。建议只记录请求 ID、调用来源、异常类型和必要的字段摘要。
注意点
- 不要把“内网接口”当作安全边界,尤其是 MQ、任务系统、配置中心这类组件。
- 不要只升级根项目依赖,要确认打包后的最终依赖版本。
- 不要在公共 JsonUtil 里提供过于宽泛的 parse 方法,否则业务迟早会滥用。
- 不要允许外部参数指定 Java 类名或反序列化目标类型。
- 不要忽略后台管理功能,很多数据写入点来自运营后台或内部工具。
总结
Fastjson AutoType 风险的治理,本质上是反序列化边界治理:输入从哪里来、能否影响类型、解析后是否有校验、依赖版本是否可控。单纯升级版本只能解决一部分问题,真正长期有效的做法,是把危险调用点收敛到少数公共入口,并在代码规范和扫描规则里持续约束。
经验上,反序列化问题最容易出现在“为了兼容临时放宽限制”的地方。临时方案如果没有回收机制,几年后就会变成审计里最难解释的风险点。
推荐意见