Java 反序列化问题并不新,但在日常代码审计和应急排查里依然很常见。它的麻烦点在于:漏洞往往不只来自业务代码本身,也可能来自历史依赖、框架默认行为、缓存组件、消息队列、RPC 协议或运维临时开放的管理接口。
这篇文章不讨论攻击利用链的构造,也不提供可直接用于入侵的操作步骤。主要整理一次偏防守视角的排查方法:如何从代码、依赖、入口、运行时配置几个层面识别反序列化风险,并给出相对可落地的加固建议。
一、背景:为什么反序列化问题容易被漏掉
很多团队对反序列化的理解停留在 ObjectInputStream.readObject() 这种显式调用上,但实际项目里常见入口更分散:
- 业务代码直接使用 Java 原生序列化做对象存储或网络传输。
- Redis、MQ、RPC、缓存框架中间层自动完成对象序列化与反序列化。
- 历史接口为了兼容老客户端,保留了二进制对象传输能力。
- 第三方组件引入了存在高危反序列化利用链的依赖版本。
- 管理端点、调试接口或内部服务暴露到非预期网络范围。
反序列化风险的核心不是“使用了序列化”本身,而是“不可信输入进入了具备危险类型解析能力的反序列化流程”。审计时需要围绕这个核心判断。
二、典型场景:从一个内部接口告警说起
某次排查中,应用网关日志提示一个内部接口出现异常请求,Content-Type 是 application/octet-stream,请求体长度明显异常。该接口原本只给内网服务调用,用于接收任务对象并写入队列。
初步查看代码后发现,接口中存在如下模式:
try (ObjectInputStream ois = new ObjectInputStream(request.getInputStream())) {
TaskMessage msg = (TaskMessage) ois.readObject();
taskService.submit(msg);
}从业务角度看,这段代码很直接;从安全角度看,风险也很明显:HTTP 请求体来自网络输入,直接进入了 Java 原生反序列化流程。即使接口声称“只在内网调用”,只要边界控制存在偏差,风险就会被放大。
三、审计思路:先找入口,再看类型边界
我个人习惯把反序列化审计分成四步:入口定位、数据来源判断、类型约束检查、依赖风险确认。
1. 定位危险 API 与框架行为
代码层面可以先搜索典型 API。下面这些关键字比较常见:
ObjectInputStream
readObject
readUnshared
XMLDecoder
XStream
readValue
fromJson
parseObject
Kryo
Hessian
MarshallingRedisSerializer
JdkSerializationRedisSerializer简单 grep 可以快速做第一轮定位:
grep -RIn "ObjectInputStream\|readObject\|XMLDecoder\|XStream\|JdkSerializationRedisSerializer" ./src如果是 Maven 项目,也建议同步查看依赖树,确认是否存在历史高风险组件或不必要的序列化库:
mvn dependency:tree -DverboseGradle 项目可以使用:
./gradlew dependencies注意,很多风险点不会直接出现在 Controller 里,而是在工具类、消息消费者、缓存配置、RPC 编解码器中。
2. 判断数据是否可信
找到反序列化点后,不要立刻下结论,需要继续确认输入来源:
- 是否来自 HTTP 请求体、Header、Cookie、上传文件。
- 是否来自 MQ 消息、Redis 值、数据库字段、对象存储文件。
- 是否由外部系统、合作方系统或客户端直接写入。
- 是否经过签名校验、来源校验、结构校验和权限控制。
这里容易误判的一点是:数据库、Redis、MQ 并不天然可信。只要其中的数据可被外部链路写入,或者存在供应链、越权写入、运维误配置等问题,它们就可能成为二次触发点。
3. 检查类型约束是否有效
反序列化安全防护里,类型约束非常关键。弱约束示例通常是“先完整反序列化,再强转成业务类”。这种方式并不能阻止危险类型在反序列化过程中被解析。
Object obj = ois.readObject();
TaskMessage msg = (TaskMessage) obj;更合理的思路是在反序列化发生前限制允许解析的类。Java 9 之后可以使用 ObjectInputFilter 做白名单控制:
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.dto.TaskMessage;java.base/*;!*"
);
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();这里的 !* 表示拒绝其他未匹配类型。实际配置时需要结合业务对象使用到的基础类型、集合类型做测试,避免误伤。
如果还在使用 Java 8,可以考虑升级运行时,或者使用自定义 ObjectInputStream 覆盖 resolveClass 做严格白名单。但这类实现要谨慎,不能只靠包名前缀做粗粒度判断。
四、依赖与配置:容易被忽视的两类问题
1. 依赖版本长期不更新
不少反序列化事件并不是业务代码直接写了危险逻辑,而是依赖库存在已知问题。审计时建议至少做三件事:
- 导出依赖树,标记序列化、表达式解析、动态代理、字节码增强相关组件。
- 检查组件版本是否存在公开高危漏洞记录。
- 移除业务不需要的历史依赖,减少可被滥用的类路径面。
可以使用 OWASP Dependency-Check 做基础扫描:
dependency-check.sh --project demo-app --scan ./ --format HTML --out ./dependency-report扫描结果不能直接等同于漏洞结论,但适合用来发现“该升级的依赖一直没升级”这类问题。
2. 缓存和消息序列化配置不当
Spring 项目中一个常见问题是 RedisTemplate 默认或历史配置使用 JDK 序列化:
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setValueSerializer(new JdkSerializationRedisSerializer());
return template;
}如果 Redis 数据来源复杂,或者存在跨系统共享写入,就要格外谨慎。更推荐使用明确结构的 JSON 序列化,并配合类型白名单、字段校验和业务 schema 约束,而不是让反序列化器自由恢复任意对象图。
示例配置方向如下:
ObjectMapper mapper = new ObjectMapper();
mapper.disableDefaultTyping();
mapper.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfSubType("com.example.dto")
.build(),
ObjectMapper.DefaultTyping.NON_FINAL
);需要注意,Jackson 的多态反序列化如果配置不当也可能引入风险。能不用全局多态就尽量不用;确实需要时,应限制到明确包路径和明确 DTO 类型。
五、加固建议:从“能跑”改到“可控”
实际落地时,不建议只修一个点。更稳妥的方式是把反序列化入口收敛,并补上边界控制。
- 优先替换协议:跨服务传输尽量使用 JSON、Protobuf、Avro 等结构明确的格式,不使用 Java 原生序列化作为对外协议。
- 强制类型白名单:无法短期替换的反序列化点,必须配置类白名单,并限制深度、数组大小、引用数量。
- 输入来源校验:内部接口也要做鉴权、签名、时间戳、防重放和调用方身份校验。
- 网络边界收敛:管理端、任务接口、内部 RPC 不应直接暴露在办公网、测试网或公网。
- 依赖最小化:移除不再使用的高风险库,避免类路径中存在过多历史包。
- 运行时监控:关注异常序列化流量、反序列化异常、接口 500 激增、非常规 Content-Type 请求。
JDK 层面可以通过全局过滤器增加兜底控制。示例:
-Djdk.serialFilter=maxdepth=20;maxrefs=10000;maxbytes=1048576;com.example.dto.*;java.base/*;!*这个配置不是万能的,仍需结合业务测试。尤其是复杂对象图、集合嵌套较深的系统,需要先在预发环境观察。
六、排查清单
| 检查项 | 关注点 |
| 危险 API | 是否存在 ObjectInputStream、XMLDecoder、XStream、Hessian、Kryo 等入口 |
| 输入来源 | 是否来自 HTTP、MQ、Redis、文件上传、数据库可控字段 |
| 类型限制 | 是否在反序列化前做白名单,而不是反序列化后强转 |
| 依赖风险 | 是否存在历史高危组件、无用依赖、长期未升级版本 |
| 边界控制 | 接口是否有鉴权、签名、网络访问限制 |
| 日志监控 | 是否能发现异常请求体、反序列化异常和接口错误率变化 |
七、几个容易踩的坑
- 只修 Controller,不查异步链路:很多触发点在消费者、定时任务、缓存读取处。
- 认为内网等于可信:内网横向、代理转发、测试环境暴露都会改变风险边界。
- 白名单写得过宽:例如直接允许整个业务根包,可能把不该解析的类型也放进去。
- 依赖扫描后不复核:扫描工具会有误报和漏报,关键组件要人工确认使用路径。
- 只升级不验证:序列化框架升级可能改变兼容性,需要准备回归用例。
八、总结
Java 反序列化治理的重点不是记住某条利用链,而是把“不可信输入”和“危险对象恢复能力”隔离开。审计时建议沿着入口、来源、类型、依赖、边界五个方向展开,优先处理直接暴露在网络链路上的反序列化点。
如果项目历史较长,短期内很难完全替换序列化协议,可以先做三件事:加类型白名单、收紧网络和鉴权边界、清理高风险依赖。这样虽然不是最终状态,但能明显降低被异常输入触发的概率,也方便后续逐步重构。
推荐意见