我经常会在技术群里看到类似这样的对话:有人贴出一条漏洞公告,问“fastjson 又报漏洞了,要不要升级到 1.2.84?”,然后底下会有人回复“升了也没用,换 Jackson 吧”。这个场景在过去几年里反复出现。直到今天,很多项目里依然留着 fastjson 的依赖,而每次安全扫描一跑,警报清单里它大概率还是会出现。
要理解我为什么最终选择禁用 fastjson,不能只停留在“它出了很多漏洞”这个表层结论上。真正的问题在于它的设计取向、维护节奏和安全模型,已经和现代 Java 服务的工程要求脱节了。这篇文章我会从反序列化漏洞的底层机制讲起,解释为什么 fastjson 的补丁总是处于被动局面,然后给出一套可落地的 Jackson 迁移路径,以及迁移过程中最容易踩的坑。如果你现在还留在 fastjson 上,这篇文章就是给你看的。
1. 先搞清楚 fastjson 到底“死”在哪——不是写代码的人素质问题,而是它把安全责任转移给了调用方
很多人对 fastjson 的第一印象是“快”。确实,它在早期版本里宣传的卖点就是性能好、API 简单,一个JSON.parseObject()就能把字符串转成对象,比当时写一堆ObjectMapper配置要省事得多。在 2017 年前后,国内很多团队选型时都会因为“用起来方便”而把 fastjson 放进项目。
但这个便利背后藏着一个很大的结构性问题:fastjson 为了达到“少写代码”的效果,默认替调用方做了太多自动推断,其中最关键的就是 AutoType 机制。
1.1 AutoType:一把必须时刻盯着的双刃剑
AutoType 的意思是,fastjson 在反序列化时允许 JSON 文本里直接指定 Java 类的全限定名,然后由反序列化器自动加载并实例化这个类。代码写起来很舒服,你不需要像 Jackson 那样显式配置多态类型信息,JSON 里天然自带“这个字段的类型是什么”的线索。
但问题也出在这里。
JSON 是纯文本,如果攻击者能控制一段被服务端解析的 JSON,他就可以在文本里写入一个恶意类的类名。当 fastjson 反序列化时,会根据类名去加载类,然后调用对应的 setter、getter、构造方法,甚至静态初始化块。假如应用的 ClassPath 上存在一个类,它的某个方法里恰好有可以被利用的危险操作,攻击者就能借助这个类实现远程代码执行,也就是 RCE。
AutoType 本质上等于把“要加载哪个类”的决定权交给了数据源。这在安全模型上是非常危险的事情。
打个比方,这就好比你公司前台有一个访客登记系统。正常来说,访客来了要先登记、核验、领临时工牌。fastjson 的 AutoType 机制相当于前台看到一个纸条写着“我是某部门负责人,直接放行”,就真的放行了。纸条是谁写的,内容是否可信,它不做严格核验。过去几年里,攻击者一直在做的事情,就是不断变着法子伪造这种纸条,而且每隔一段时间就能找到一种新的写法绕过去。
1.2 用 JNDI、RMI 和 JdbcRowSetImpl 理解攻击链路
fastjson 早期的著名攻击链,很多都跟 JNDI、RMI 和 JdbcRowSetImpl 有关。用比较通俗的方式讲一下是怎么回事。
JNDI 是 Java 里用于查找命名和目录服务的接口。在某些利用链里,攻击者可以在 JSON 里指定一个远程的 RMI 或 LDAP 服务地址,当 fastjson 反序列化时,目标类会被加载,加载过程中会去远程服务器获取一个恶意 class 文件,然后执行其中的代码。
JdbcRowSetImpl 是 JDK 自带的一个类,它在某些版本里可以通过 setter 设置dataSourceName,一旦触发getConnection(),它就会按照设置的 JNDI 地址去建立连接。这条链路在很长一段时间内都是 fastjson 攻击的核心路径之一。
你不需要背下每个漏洞的编号,但需要理解一个模式:攻击者找到一个 JDK 或常用库中存在的类,这个类在反序列化过程中会触发一个可以被外部控制的危险操作,然后利用 fastjson 的 AutoType 机制把类加载进来。每堵住一个类,攻击者就换一个类。这就是为什么 fastjson 的安全补丁总在追着漏洞跑——因为攻击面不是 fastjson 本身的代码,而是它开放给外部类加载的能力。
1.3 升级到 1.2.84 确实有意义,但不要把它当成终点
fastjson 1.2.84 是 1.x 系列中一个比较重要的安全加固版本。它新增了在异常路径上的类名、依赖和堆栈的深度检查,也对java.lang.Exception这条攻击链做了额外处理。在 1.2.80 之后、1.2.83 之前,出现过一些新的利用方式,1.2.83 和 1.2.84 就是为了堵住这些新出现的问题而发布的。
但是,如果你把每次“升级到最新版”当成安全策略,就会陷入一个非常疲惫的循环:
- 下个新版本出现,安全团队要求升级。
- 升级后跑回归测试,发现某些序列化行为有变化。
- 还没等完全验证完,又有新的漏洞公告出来。
而且,fastjson 1.x 的维护模式已经发生了变化。项目团队把主要精力转向了 fastjson 2,1.x 进入了一种“只修高危漏洞、不做大功能演进”的状态。这意味着你等来的每一个新版本,很可能又是上一次绕过尝试的应急响应。
我的判断是:如果项目还在用 fastjson 1.x,今天最该做的事情不是判断 1.2.84 够不够安全,而是启动一个迁移计划,把核心序列化依赖从 fastjson 上挪走。原因后面会展开。
2. 反序列化漏洞的底层逻辑:为什么同一个套路能反反复复出现
很多人会有个困惑:为什么 fastjson 已经修了这么多轮,还能不断出问题?是不是写代码的人不认真?其实最深层的原因,不是某一行代码写错了,而是“允许 JSON 指定类名”这个设计方向,天然就会持续产生安全对抗空间。
2.1 反序列化为什么是安全重灾区
Java 对象在内存里是类结构加字段值,而 JSON 只是一段纯文本。反序列化要做的事情,就是把纯文本“还原”成 Java 对象。这个还原过程有四个隐形的风险点:
- 类加载:文本里的类名会被 class loader 解析。如果类的来源不可信,第一个危险就出现了。
- 方法调用:反序列化不只是赋值字段,它还可能触发 setter、构造方法、
readObject、getter等。这些方法里如果写了敏感逻辑(比如建立连接、执行命令、读取文件),就会被间接调用。 - 嵌套对象:一个对象里套着另一个对象,反序列化时是递归处理的。如果嵌套深度和类型数量没有限制,很容易构造出超大递归或类型混乱的请求。
- 类型混淆:如果同一个字段在不同情况下可以被反序列化成不同类型,攻击者就可能用一个“看似安全”的类去触发另一个类的危险逻辑。
所谓“反序列化漏洞”,多数时候不是 JSON 解析器本身的代码有问题,而是它把攻击者输入的数据传递给了应用里那些“有副作用”的方法。fastjson 的 AutoType 把这个风险放大了,因为类名是外部的、不可信的,而且默认机制里没有足够严格的校验。
2.2 黑名单思路为什么总是慢半拍
fastjson 历史上采用过黑名单策略,也就是维护一个“不允许反序列化的类名列表”。这个思路看起来直觉上没问题:把所有已知危险的类都禁掉,攻击者不就没办法了吗?
但工程现实里,黑名单有三个先天缺陷:
- 滞后性:只有等到某个类被人发现可以攻击,研究出利用链,漏洞报告出来,然后才能把它加到黑名单里。在这个完整链条跑完之前,攻击者已经在用了。
- 不完整性:Java 生态里类库极其庞大,JDK 自身、Spring、MyBatis、各种第三方库,每一层都有可能出现新的可利用类。你不可能枚举完所有危险类。
- 绕过容易:同一个类可能有不同的加载方式、不同的别名、不同的编码形式。攻击者只要改一种写法,黑名单就可能形同虚设。
后来 fastjson 在较新版本里默认关闭了 AutoType,引入了白名单机制,这是一个方向正确的变化。但关键问题是:fastjson 的生态和文档里仍然有一大批场景依赖 AutoType,比如接口返回值是多态类型、泛型擦除后需要保留类型信息等。一旦你在项目里依赖了自动类型解析,当新版默认关闭它时,你的老代码可能直接抛异常。这就是为什么很多项目升级 fastjson 时,总要加一行ParserConfig.getGlobalInstance().setAutoTypeSupport(true)或类似的配置。这一打开,安全防线又出现缝隙了。
2.3 时间就是最大的成本:每个补丁背后都是整个团队的熬夜史
我曾经在一个业务系统里经历过 fastjson 漏洞应急。安全团队拿到公告后,第一件事是找出所有引入 fastjson 的组件和代码位置,然后评估哪些接口暴露在公网,哪些即使内网调用也有风险。接下来的问题更麻烦:升级到新版后,线上有些接口的序列化输出格式变了,有的 parseObject 行为也变了,还得临时调整代码。那段时间每次发布都很紧张,不是因为功能复杂,而是因为你不知道这个“安全升级”会不会带来隐藏的兼容性破坏。
单次漏洞响应的时间成本,往往被严重低估。表面上是“升级一个依赖版本号”,实际上要经历依赖冲突排查、回归测试、灰度发布、线上观察、问题修复,整个周期短则一天,长则一周。如果这样的流程每隔几个月就来一轮,团队的技术债会越积越厚。
这就是我为什么说,fastjson 的死结不在漏洞数量,而在它的安全模型让使用方处于持续被动响应的状态。相比之下,Jackson 的做法虽然需要调用方多写一点配置,但它把安全边界收敛到了一个更清晰的位置。
3. 1.2.84 之后:为什么“升级到最新版”这套思路彻底失效了
在 fastjson 1.2.83 和 1.2.84 发布的时候,官方公告里的语气已经和早期不太一样了。你从版本更新内容里能明显看到,它越来越多地在做安全加固,而不是在增加新功能。搜索热词里也高频出现“fastjson 1.2.84”和“fastjson 迁移为 jackson”,说明相当多团队已经把目光从“要不要升级”移到了“怎么迁移”。
3.1 版本序列背后的事实:1.x 已经进入生命周期末期
需要说明的是,我并不能替你确认某个版本之后官方还会不会再发布新版本,因为版本计划属于项目方动态信息。但从实际工程经验看,一个组件一旦从“功能迭代”切换到“漏洞修补”,它的寿命就在倒计时了。fastjson 2 是一个重新设计架构的项目,它采用了新的序列化器、新的安全模型,也换了新的包名(com.alibaba.fastjson2)。如果你继续守在 fastjson 1.x 上,未来要面对的不只是漏洞,还有社区活跃度下降、新特性缺失、知识积累减少这些隐性问题。
3.2 “升级到 1.2.84,然后配置安全模式”这个混合状态很难长期维护
有些团队的做法是:把 fastjson 升级到最新版,同时打开 safeMode,或者把 AutoType 关掉,再配合 RASP、WAF 等外部防护。这种多层防护的思路本身没有错,但问题在于 fastjson 的某些功能依赖 AutoType,你一旦把它关掉,就得检查所有反序列化代码是否还能正常工作。很多老项目里parseObject(String, Class)的用法,短期内确实不需要 AutoType,但一旦涉及泛型、多态、嵌套类型,事情就复杂了。混合状态意味着你需要维护一套“哪些接口开了 AutoType、哪些没开”的心智模型,这在团队人员流动时很容易失传。
3.3 延迟迁移的最大风险是“知道该迁移的人正在变少”
技术圈有一个规律:一个组件的安全问题被讨论得越久,真正能把它讲清楚的人就越少。因为早期踩过坑的人可能已经换项目了,新来的人只看到一个依赖,不知道背后的风险史。等到某个新漏洞爆发时,团队里可能找不出一个能独立评估影响范围的人。
所以,我的建议是尽早做这件事,趁现在还有足够多的参考资料和真实踩坑经验,把迁移成本控制在一个可控范围内。越往后拖,不仅要面对 fastjson 自身的问题,还要面对团队内“fastjson 知识断层”的问题。
4. 从 fastjson 迁到 Jackson:核心思路是收敛边界,不是照抄 API
选择 Jackson 并不是因为它完美无缺,而是因为它的默认安全模型更清晰:Jackson 默认不启用多态反序列化。如果 JSON 文本里没有显式的类型信息,你就不能指定要加载哪个类。这从根本上避免了“外部数据控制类加载”的问题。
当然,Jackson 也支持多态。它需要你通过activateDefaultTyping或@JsonTypeInfo这类机制,显式地告诉序列化器哪些字段、哪些类型需要保留类型信息。但关键区别是:这个能力是“按需开启”的,默认关闭。这一点和 fastjson 的设计取向完全相反,也决定了两个组件的安全基线完全不同。
4.1 迁移前必须搞清楚自己的 fastjson 用在了哪里
很多项目里,fastjson 不只是被直接调用,还可能被其他中间件间接依赖。比如某些 RPC 框架、缓存组件、数据同步工具,内部可能默认用了 fastjson 做序列化。这种间接依赖是最容易遗漏的,因为你没有直接写过import com.alibaba.fastjson,但程序运行起来时类还是被加载了。
迁移前建议做一次全量扫描:
- 在代码仓库里搜索
com.alibaba.fastjson,定位所有 import。 - 搜索
JSON.toJSONString、JSON.parseObject、JSON.parseArray、JSONObject、JSONArray等高频 API。 - 用
mvn dependency:tree或 Gradle 的dependencyInsight找出所有传递依赖 fastjson 的组件。 - 检查是否有工具类、配置类、序列化器里注册了 fastjson 的全局配置,比如
ParserConfig、SerializeConfig、@JSONField注解等。
这一步做完,你才能知道迁移的工作量到底有多大。如果一个项目里 fastjson 只出现在少量工具类中,迁移其实很快;如果它已经深嵌入自定义序列化逻辑里,就需要分阶段处理。
4.2 核心 API 对照:从 JSON.parseObject 到 ObjectMapper
先给一张最常用的 API 对照表,方便在迁移时快速找到对应写法:
| 功能 | fastjson 示例 | Jackson 示例 |
|---|---|---|
| 对象转 JSON 字符串 | JSON.toJSONString(obj) | objectMapper.writeValueAsString(obj) |
| JSON 字符串转对象 | JSON.parseObject(str, User.class) | objectMapper.readValue(str, User.class) |
| JSON 字符串转 List | JSON.parseArray(str, User.class) | objectMapper.readValue(str, new TypeReference<List<User>>() {}) |
| JSON 字符串转 Map | JSON.parseObject(str, Map.class) | objectMapper.readValue(str, new TypeReference<Map<String, Object>>() {}) |
| 序列化时忽略 null | @JSONField(serialzeFeatures = SerializerFeature.WriteMapNullValue) | objectMapper.setSerializationInclusion(Include.NON_NULL)或@JsonInclude |
| 字段别名 | @JSONField(name = "user_name") | @JsonProperty("user_name") |
| 日期格式 | @JSONField(format = "yyyy-MM-dd HH:mm:ss") | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
4.3 几个必须重新设计的差异点
Jackson 并不是 fastjson 的“一键平替”,有几处行为差异需要你在迁移时重构代码逻辑。
第一,泛型信息的处理。fastjson 里写JSON.parseObject(str, new TypeReference<List<User>>() {})是老用法,Jackson 的写法几乎一样,但如果你之前写的是JSON.parseObject(str, List.class),那么拿到的 List 里元素会被解析成 LinkedHashMap,而不是 User 对象。Jackson 也是一样的,readValue(str, List.class)不会保留元素类型。所以在迁移时,凡是涉及泛型集合、泛型对象的代码,都必须统一使用TypeReference。
第二,日期时间格式的默认差异。fastjson 对java.util.Date的默认输出格式通常是时间戳或带毫秒的时间字符串,Jackson 默认输出的是 ISO 8601 格式。这个差异会导致接口返回给前端的数据格式变化。迁移时最好是统一在ObjectMapper里配置全局日期格式,比如yyyy-MM-dd HH:mm:ss,或者使用JavaTimeModule来处理 Java 8 的LocalDateTime。
第三,字段命名策略。有些项目里 fastjson 默认输出的是驼峰字段userName,但当前端要求下划线user_name时,通常依赖@JSONField注解。Jackson 里对应的做法是@JsonProperty,但如果接口特别多、字段特别多,逐个加注解不现实,更建议配置PropertyNamingStrategies.SNAKE_CASE这类全局命名策略。这里要注意,全局策略会影响所有字段,包括嵌套对象,所以要先在小范围接口上验证。
第四,枚举的序列化方式。fastjson 默认可能输出枚举的 name 或者 ordinal,Jackson 也有自己的默认行为。如果项目里枚举很多,而且枚举值在数据库或外部接口里有固定存储,迁移时要保证序列化前后枚举的取值一致,避免出现“存的是 1,反序列化后变成 2”这类问题。
4.4 建议的分阶段迁移路径
不要在某个周五晚上一次性把全项目的 fastjson 替换成 Jackson,这会成为一场灾难。更稳妥的做法是分四步走:
- 先加一个统一的 ObjectMapper 配置类。在这个类里统一设置日期格式、命名策略、null 处理、未知字段处理、自定义序列化器。所有新代码都基于这个配置实例进行操作。
- 挑一个低风险模块做试点。最好是一个内部接口、日志处理、数据同步任务这类不直接暴露给公网、影响范围有限的模块。在这个模块里完成 fastjson 到 Jackson 的替换,验证输出差异和兼容性。
- 批量替换直接调用点,但不急着删依赖。代码里大量
JSON.parseObject和JSON.toJSONString可以先用自定义工具类包一层,比如JsonUtils,内部实现从 fastjson 换成 Jackson。这样即使外部传参方式不变,也能逐步切换底层实现。 - 最后清理间接依赖。用依赖分析工具找出哪些中间件还在传递依赖 fastjson,能升级组件的就升级组件,不能被替代的就单独评估风险,决定是否需要用隔离机制处理。
注意:Fastjson 和 Jackson 的异常信息格式不太一样。迁移后不要把老的异常解析逻辑直接套用到新序列化器上,建议统一校验一遍异常处理代码。
5. 迁移落地时的排查链路和边界:真正决定成败的是细节
很多项目迁移 Jackson 之后,功能看起来正常,但上线一段时间后会冒出一些奇怪的问题。这些问题往往不是“迁移错了”,而是“迁移时不彻底”。
5.1 按这个顺序排查:输入、配置、序列化器、依赖、性能
如果在迁移过程中出现异常,不要直接搜“Jackson 报错原因”,那样效率很低。我建议按下面顺序排查:
第一步,看输入。先确认 JSON 字符串的格式是否符合预期。比如是不是带 BOM?字段命名到底是大驼峰、小驼峰还是下划线?字符串里有没有特殊字符?很多解析异常其实是输入数据格式不规范导致的。
第二步,看全局配置。检查 ObjectMapper 是否被多处重复创建。每创建一个 ObjectMapper,就相当于有一份独立的配置。如果模块 A 的 ObjectMapper 配置了日期格式,模块 B 用的是另一个没有配置的实例,输出就会不一致。在大型项目里,ObjectMapper 应该是单例,统一注入。
第三步,看自定义序列化器。fastjson 项目里你可能写了ObjectSerializer或ValueFilter,这些自定义逻辑在 Jackson 里没有完全对应的概念。排查时要确认所有自定义序列化器都生效了,而且没有作用到全局字段上。
第四步,看间接依赖。运行时如果 ClassNotFound 或者 NoSuchMethodError,先查依赖树。很多类是由应用服务器或中间件提供的,版本不一致会导致 Jackson 的类加载器加载到不同版本的类。
第五步,看性能。如果迁移后接口响应时间变长,先看是否在每次请求里创建了 ObjectMapper,再看是否序列化了重复结构,最后考虑是否需要引入JsonNode或流式 API 来减少中间对象创建。
5.2 序列化输出不一致的常见坑
迁移最容易出问题的不是反序列化,而是序列化输出变化。因为反序列化只在服务端内部处理,而序列化后的结果会直接返回给前端或写入其他系统。一个字段的命名、一个 null 值的取舍、一个日期的格式,都可能引发联调问题。
如果你无法接受所有输出一次性变化,可以在替换策略上做兼容:用 Jackson 生成新数据结构,同时保留一个 fastjson 版本的快照用于对比。写一个对比脚本,对同一批对象用两种方式序列化,然后 diff 输出,逐个解决差异。这不解决根本问题,但能让你在上线前知道自己改了哪些东西。
5.3 要不要考虑 fastjson 2
fastjson 2 是官方推出的重写版本,包名、API、内部机制都有较大变化。如果你实在不想用 Jackson,又需要保留 fastjson 的风格,fastjson 2 可能是一个过渡选择。但我的观点是:既然已经从 fastjson 1.x 迁移出来,就没有必要再迁移到 fastjson 2。因为核心问题——AutoType 和安全性之间的张力——虽然在 fastjson 2 中有改善,但设计思路仍然延续了“让 JSON 解析更省事”的方向。相比起来,Jackson 的默认安全模型更符合一个长期维护项目对稳定性和可控性的要求。
注意:不要因为网上有人说“fastjson 2 更安全”,就直接在核心链路上替换 fastjson 2。先在小项目上验证它的序列化兼容性和潜在依赖冲突,再做决定。
5.4 长期维护视角:把序列化逻辑收口到统一层
迁移完成之后,最值得做的一件事是:建立项目内部的统一 JSON 工具层。所有业务代码不直接依赖具体的ObjectMapper或某个 JSON 库,只依赖JsonUtils工具类。这样做有三个好处:
- 以后如果要再替换 JSON 库,只需要改一个类。
- 可以在工具类里统一加日志、异常处理、脱敏逻辑,而不是散落在各业务代码里。
- 新同事入职后,只需要知道“项目里 JSON 统一用这个工具类”,不用关心底层实现。
这个“统一收口”的思路,其实比选哪个 JSON 库更重要。很多项目反复在序列化组件上踩坑,根本原因是每个开发都有自己写 JSON 转换的方式,同一个项目里既有 fastjson、又有 Jackson、还有 Gson,出问题后极难排查。
6. 回到最初的问题:fastjson 还能不能用
如果你问我,fastjson 还能不能用,我的回答是:能用,但我不建议在新的核心项目里继续使用。它能用,指的是在封闭内网、无外部输入、低安全要求、快速原型这类场景下,它的 API 便捷性依然有优势。但它不适合作为公网服务、核心业务链路的默认序列化方案,因为它的历史安全模型和补丁节奏决定了你会持续处于被动状态。
如果把技术决策看成一笔长期投资,重仓 fastjson 的收益是“初期接入快、写代码顺”,但成本是“持续的漏洞响应、升级验证、心智负担和安全风险”。而迁移到 Jackson 的成本是“一次性改造”,收益是“安全边界清晰、社区生态成熟、长期维护确定性更高”。
所以,我建议你现在就做三件事:
- 在代码仓库里全量搜索
com.alibaba.fastjson,把使用清单列出来。 - 挑一个内部模块,用 Jackson 的小规模试点跑一遍,记录所有差异点。
- 把项目里所有 JSON 相关调用收口到一个统一工具层,为后续替换做好准备。
你不用在一天之内完成全部迁移。但你可以从今天开始,让项目朝着“不再依赖 fastjson”的方向挪动。这个过程不会太轻松,但几年后回头看,你会感谢自己早做了这个决定。