跳转到帖子

背景

Fastjson 相关安全问题在很多老系统里仍然比较常见,尤其是一些历史 Java Web 项目、内部管理平台、网关服务和定时任务系统。实际排查时,很多团队只关注“版本是否存在漏洞”,但从代码审计角度看,风险往往不止来自组件版本,还包括反序列化入口、AutoType 配置、全局 ParserConfig、异常日志泄露以及业务对象边界不清晰等问题。

这篇文章整理一套偏防守和审计视角的排查方法,适合用于内部资产自查、代码审计复核和安全加固,不涉及攻击利用细节。

问题场景

在一次老项目安全复核中,发现项目依赖里存在较早版本的 fastjson,同时代码中大量使用 JSON.parseObject、JSON.parseArray 和 JSONObject.toJavaObject。项目本身并没有直接暴露“反序列化接口”,但多个控制器会接收前端传入的 JSON 字符串,再转换为业务对象。

这类场景的主要问题是:开发人员通常认为“只是解析 JSON”,但如果组件版本较老、配置不当,或者解析目标类型不受控,就可能扩大攻击面。

常见风险点

  • 组件版本过旧:老版本 Fastjson 曾多次出现反序列化相关安全问题,不能只依赖黑名单机制。
  • 开启 AutoType:历史代码中可能通过全局配置开启 AutoType,导致类型解析边界变宽。
  • 直接解析用户输入:将 HTTP 参数、消息队列内容、文件内容直接交给 JSON.parseObject 处理。
  • 解析目标类型不明确:使用 JSONObject、Object、Map 等宽泛类型接收外部输入。
  • 依赖链复杂:项目中存在大量第三方依赖,即使业务代码简单,也可能扩大反序列化后的类型可达范围。
  • 日志记录不当:异常栈、原始请求体、内部类名泄露,可能帮助外部人员判断技术栈。

代码审计关注点

审计时不建议只做关键词搜索后简单下结论。更有效的方法是先定位输入边界,再追踪 JSON 解析链路,最后确认解析目标类型和安全配置。

可以优先搜索以下调用点:

JSON.parseObject(...)
JSON.parseArray(...)
JSONObject.parseObject(...)
JSONObject.toJavaObject(...)
JSON.parse(...)
ParserConfig.getGlobalInstance()
setAutoTypeSupport(true)

如果项目较大,可以结合 ripgrep 或 IDE 全局搜索:

rg "parseObject|parseArray|toJavaObject|setAutoTypeSupport|ParserConfig" src/

审计时重点看三个问题:

  • 传入 JSON 解析方法的数据是否来自外部可控输入。
  • 解析时是否指定了明确、稳定的业务 DTO 类型。
  • 项目是否存在全局开启 AutoType 或自定义反序列化配置。

一个典型的风险写法

下面这种写法在历史系统中比较常见。它的问题不一定等同于“必然存在漏洞”,但从审计角度看,边界过宽,后续维护风险较高。

public Object handle(String body) {
    return JSON.parseObject(body, Object.class);
}

更稳妥的写法是定义明确的请求对象,并对字段做白名单校验:

public SubmitRequest handle(String body) {
    SubmitRequest request = JSON.parseObject(body, SubmitRequest.class);
    validate(request);
    return request;
}

如果是在 Spring MVC 场景中,更建议让框架完成请求体到 DTO 的绑定,并配合参数校验,而不是在控制器里手动解析任意 JSON 字符串。

@PostMapping("/submit")
public Result submit(@Valid @RequestBody SubmitRequest request) {
    return service.submit(request);
}

依赖版本排查

Maven 项目可以先查看依赖树,确认 fastjson 是否直接引入,还是被其他组件间接引入。

mvn dependency:tree | grep fastjson

Gradle 项目可以使用:

./gradlew dependencies | grep fastjson

排查时需要注意两点:

  • 同一个项目可能存在多个模块,不同模块依赖版本不一致。
  • 打包后的实际依赖以最终产物为准,不能只看某一个 pom.xml。

如果项目仍在使用 Fastjson 1.x,建议结合业务兼容性评估升级到维护状态更好的版本,或者迁移到 Jackson、Gson 等替代方案。升级前应准备回归测试,重点覆盖日期格式、枚举、泛型、字段别名、空值处理等场景。

安全配置建议

如果业务没有明确需求,不建议开启 AutoType。历史项目中可以重点确认是否存在如下配置:

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

如果发现全局开启,应先确认是谁依赖了这个行为。实际处理时不要简单删除后直接上线,建议按以下顺序进行:

  • 定位依赖 AutoType 的业务接口或消息消费逻辑。
  • 将宽泛对象解析改为明确 DTO 解析。
  • 补充回归测试,覆盖历史 JSON 样例。
  • 关闭全局 AutoType,并灰度发布观察。

对于必须进行多态反序列化的业务,应尽量限制在内部可信数据流中,并对允许的类型做显式白名单管理,避免把任意外部输入交给类型解析逻辑。

接口层防护要点

很多风险最终都会落到接口输入边界。除了组件升级,接口层也应做基本约束。

  • 限制请求体大小:避免超大 JSON 导致解析资源消耗。
  • 限制字段集合:不要接受业务无关字段,减少对象污染和兼容性隐患。
  • 统一异常处理:不要将完整异常栈和内部类名返回给客户端。
  • 记录必要审计日志:保留接口、时间、来源、错误类型,不建议原样记录敏感请求体。
  • 配置 WAF 或网关规则:用于发现异常 JSON 特征,但不要把它作为唯一防线。

日志与监控排查

在事件复盘或风险自查中,可以从日志里寻找异常解析行为。常见线索包括:

  • JSON 解析异常频繁出现。
  • 异常日志中出现非业务类名。
  • 某些接口收到结构异常复杂的请求体。
  • 同一来源短时间内访问多个 JSON 接口。
  • 网关层出现大量 400、500 状态码。

建议将 JSON 解析异常单独归类,避免被普通业务异常淹没。例如在统一异常处理里对反序列化失败、参数绑定失败做分类统计。

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Result> handleJsonError(HttpMessageNotReadableException e) {
    log.warn("json parse failed: {}", e.getClass().getSimpleName());
    return ResponseEntity.badRequest().body(Result.fail("invalid request body"));
}

灰度加固流程

生产系统加固不建议一次性大改。比较稳妥的流程如下:

  • 第一步,梳理 fastjson 依赖版本和调用点。
  • 第二步,标记外部可控输入进入 JSON 解析的位置。
  • 第三步,将 Object、JSONObject、Map 等宽泛解析逐步改成 DTO。
  • 第四步,关闭不必要的 AutoType 配置。
  • 第五步,升级依赖并进行兼容性回归。
  • 第六步,上线后观察异常率、接口错误率和业务关键指标。

实际项目里,最容易出问题的是“升级后序列化格式变化”。例如日期格式、BigDecimal 精度、空字段输出策略,都会影响前后端或上下游服务。因此升级前最好收集一批真实但脱敏的历史请求样例,做自动化对比。

审计清单

检查项建议
Fastjson 版本确认是否为历史高风险版本,优先升级到安全版本或迁移替代库
AutoType默认关闭,避免全局开启
解析入口外部输入进入 JSON 解析前应有明确边界
目标类型优先使用明确 DTO,避免 Object、JSONObject 承接复杂输入
异常处理客户端返回通用错误,服务端保留必要审计信息
依赖范围确认最终打包产物中的实际依赖版本
测试覆盖升级前准备日期、枚举、泛型、空值等兼容性测试

注意点

  • 不要把“没有公开接口”当作安全结论,消息队列、文件导入、内部 RPC 同样可能是输入面。
  • 不要只看代码仓库里的依赖声明,最终运行环境里的 jar 才是实际结果。
  • 不要简单依赖黑名单。安全边界应建立在明确类型、最小权限和输入约束上。
  • 不要在日志中完整打印用户提交的 JSON,尤其是包含 token、手机号、身份证号等敏感字段时。
  • 不要在不了解业务兼容性的情况下直接替换 JSON 库,否则可能引入隐蔽的业务问题。

总结

Fastjson 风险排查的重点不是记住某个漏洞编号,而是建立一套稳定的审计方法:先看依赖版本,再看输入边界,接着看解析目标类型和全局配置,最后结合日志监控和回归测试完成加固。

从长期维护角度看,最有效的做法是减少宽泛反序列化入口,使用明确 DTO 承接外部输入,关闭不必要的 AutoType,并保持依赖版本可控。这样即使后续组件再出现新的安全问题,系统暴露面也会小很多。

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

0篇意见

推荐意见

没有意见。

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