跳转到帖子

背景

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 和监控防止旧依赖回流。这样做虽然不如单次扫描快,但长期看更能降低同类问题反复出现的概率。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…