背景
Fastjson 相关漏洞已经不是新问题,但在真实环境里,它仍然经常以“历史依赖残留”“间接依赖引入”“测试接口未下线”的形式出现。很多团队做过一次升级后就以为结束了,实际排查时会发现:不同服务版本不一致、fat jar 里打进了旧包、某些后台任务仍在使用危险配置。
这篇文章整理一次偏防守视角的排查过程,重点放在如何确认风险、如何定位代码与依赖、如何做加固和验证。不会讨论可用于未授权利用的细节,只保留对代码审计和安全整改有价值的内容。
问题场景
某内网环境出现一条应用侧告警:某 Java 服务在处理 JSON 请求时出现异常类加载日志,日志中伴随少量反序列化相关异常。服务本身不是公网入口,但接收来自多个内部系统的消息,使用 Fastjson 解析业务字段。
初步信息如下:
- 应用为 Spring Boot 服务,打包方式为可执行 jar。
- 服务存在多个 JSON 解析入口,包括 HTTP 接口、消息队列消费者、定时任务读取配置。
- 依赖树中存在 fastjson,但版本来源不明确,可能由公共 SDK 间接引入。
- 部分历史代码使用了全局 ParserConfig 配置。
这类场景的核心不是“复现攻击”,而是确认三件事:是否存在危险版本、是否启用了危险能力、是否有外部可控数据进入解析链路。
技术分析
Fastjson 历史风险的根因之一,是 JSON 解析过程中对类型信息处理不当,在特定版本和配置下可能触发非预期类加载或对象构造。风险高低通常取决于以下几个因素:
- Fastjson 版本是否处于已知风险区间。
- 是否启用 autoType 或类似的类型自动识别能力。
- 解析方法是否允许接收外部可控 JSON。
- 服务运行环境中是否存在可被滥用的依赖链。
- 是否有安全边界,例如鉴权、字段白名单、消息来源校验。
实际审计时,不建议只看 pom.xml。Java 服务经常存在依赖覆盖、shade 打包、公共组件间接引入等情况,需要结合构建产物、运行时类路径和代码调用点一起看。
关键排查步骤
1. 确认构建依赖与最终产物
先从源码依赖树开始,确认 fastjson 是否为直接依赖或间接依赖:
mvn dependency:tree -Dincludes=com.alibaba:fastjson如果是 Gradle 项目,可以使用:
./gradlew dependencies --configuration runtimeClasspath | grep -i fastjson但仅看依赖树不够。建议继续检查最终 jar 包内容,避免构建插件或公共包把旧版本打进去:
jar tf app.jar | grep -i fastjson如果是 Spring Boot fat jar,通常还需要看 BOOT-INF/lib:
jar tf app.jar | grep 'BOOT-INF/lib/.*fastjson'在容器环境里,也可以进入镜像或运行容器确认实际文件:
find /app -iname '*fastjson*.jar' -o -iname '*.jar' | head排查结论要记录“源码依赖版本”和“运行时实际版本”。两者不一致时,以运行时实际版本为准。
2. 搜索危险配置与解析入口
代码层面重点关注两类内容:全局配置和解析方法调用。可以先做基础检索:
grep -R "ParserConfig" -n src/main/java
grep -R "autoType" -n src/main/java
grep -R "JSON.parse" -n src/main/java
grep -R "parseObject" -n src/main/java
grep -R "parseArray" -n src/main/java常见风险点包括:
- 显式开启 autoType。
- 对外部输入直接调用 JSON.parse 或 parseObject。
- 使用 Object、Map、JSONObject 作为宽泛接收类型,后续再做强制转换。
- 消息队列消费端默认信任消息内容,没有来源校验或字段校验。
- 公共工具类封装了解析逻辑,业务代码看起来无风险,但底层配置不安全。
较稳妥的方式是把解析入口列成表,逐个判断数据来源和目标类型。
| 入口类型 | 关注点 | 建议 |
|---|---|---|
| HTTP 请求体 | 是否来自未信任客户端 | 使用 DTO 明确字段,开启参数校验 |
| 消息队列 | 消息生产方是否可控 | 校验 topic、签名、字段 schema |
| 配置中心 | 是否存在低权限可修改配置 | 限制配置变更权限,解析前做 schema 校验 |
| 日志回放/任务导入 | 历史数据是否可信 | 导入前清洗,避免直接解析任意 JSON |
3. 判断是否存在真实暴露面
漏洞风险评估不能只停留在“版本命中”。建议按下面的顺序判断:
- 输入是否可被低权限用户、外部系统或不可信服务控制。
- 输入是否进入 Fastjson 解析链路。
- 解析时是否允许类型信息参与对象构造。
- 运行环境是否存在危险依赖组合。
- 是否有日志、WAF、网关、鉴权、签名等防护降低可达性。
如果只能确认旧版本,但没有外部可控输入,也不代表可以忽略。它仍然属于供应链和历史依赖风险,应纳入升级计划;只是处置优先级可以低于外部可达入口。
4. 安全整改建议
整改优先级一般按“升级依赖、关闭危险能力、收敛解析类型、补充输入校验、增加监控”处理。
依赖升级应优先采用官方维护版本,并通过依赖管理统一锁定,避免某个公共 SDK 拉回旧版本:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>安全基线版本</version>
</dependency>
</dependencies>
</dependencyManagement>代码上尽量避免把外部 JSON 解析成 Object 或 JSONObject 后再动态处理。更推荐使用明确的 DTO:
public class OrderEvent {
private String orderId;
private String status;
private Long timestamp;
// getter / setter
}OrderEvent event = JSON.parseObject(jsonString, OrderEvent.class);
validate(event);如果项目允许替换 JSON 库,也可以评估 Jackson、Gson 等方案,但不要把“换库”当成唯一修复。任何反序列化组件在错误配置下都可能产生风险,关键仍然是类型收敛和输入校验。
5. 验证整改是否生效
整改后建议做三类验证。
- 构建验证:依赖树中不存在旧版本,最终 jar 中不存在旧 fastjson 包。
- 代码验证:无危险全局配置,无宽泛解析入口,关键入口有 DTO 与字段校验。
- 运行验证:灰度环境日志无异常类加载、解析异常数量下降、告警规则仍可触发测试事件。
可以在 CI 中加入简单的依赖检查,防止后续回退:
mvn dependency:tree -Dincludes=com.alibaba:fastjson | tee dependency-fastjson.txt
grep -E "fastjson:jar:" dependency-fastjson.txt也可以结合 SCA 工具做版本基线控制。这里不建议只依赖工具结论,因为很多工具只能识别版本,无法判断业务入口是否真实可达。
注意点
- 不要只升级根项目 pom,公共 SDK、插件、shade 包也要检查。
- 不要在生产环境做带破坏性的漏洞验证,使用日志、代码审计和受控测试环境确认即可。
- 不要把内网服务默认视为可信,消息队列、配置中心、任务导入同样可能成为输入入口。
- 不要为了兼容历史数据保留宽泛解析逻辑,可以增加迁移脚本或兼容 DTO,而不是放开类型解析。
- 升级后要关注业务兼容性,JSON 字段大小写、日期格式、空值处理都有可能影响老逻辑。
总结
Fastjson 历史漏洞的治理重点,不只是“版本是否命中”,而是把依赖、配置、调用点、数据来源串起来看。真实环境中最容易漏掉的是间接依赖和非 HTTP 入口,例如消息队列消费者、配置解析、离线任务导入。
比较稳的处理方式是:先确认运行时真实版本,再定位所有解析入口,收敛外部输入到明确 DTO,关闭不必要的类型自动识别,最后用 CI 和监控防止旧依赖回流。这样做虽然不如单次扫描快,但长期看更能降低同类问题反复出现的概率。
推荐意见