背景
-
现阶段公司会进行季度的安全巡检,扫描出来的 Java 相关漏洞,无论是远程代码执行、还是 JNDI 注入,基本都和 Java 的序列化机制有关。本文简单梳理了一下序列化机制相关知识,解释为什么这么多漏洞都和 Java 的序列化有关,以及后续怎么避免这些安全漏洞,减少版本升级工作量。同时能基于本文的知识,在看到序列化漏洞后,简单评估该漏洞对自身应用的影响
为什么需要序列化
-
序列化主要是提供了一种机制,方便数据在网络之间进行传输,或者独立于程序存储在本地磁盘。
-
序列化的使用场景很广,比如服务器收到请求参数,一种处理方式是一个个解析数据,自己构建处理数据。还有一种就是直接将数据反序列化为对象。当要把对象存储到缓存时,我们可以自己解析对象生成数据保存到缓存,取出时也自己处理数据转换为对象,也可以直接借助语言的序列化机制帮我们将对象序列化为数据存储起来,从缓存里获取数据后直接反序列化转为对象。在这些场景里,很明显序列化机制能极大改善我们的编程体验
Java 序列化基础知识简述
-
最简单模式:
implements Serializable
-
我们可以只实现序列化接口,让 Java 序列化机制会帮我们处理其他的一切事情
-
基于
Serializable
接口简单定制 -
可以用
transient
指定不想序列化的数据,比如密码等敏感数据 -
可以定义自己的
writeObject()
和readObject()
方法,来自定义部分序列化和反序列化流程。注意这两个方法是私有的,却会被 Java 序列化机制自行调用 -
基于
Externalizable
接口全面定制 -
Externalizable
接口提供了两个在序列化/反序列化时自动调用的方法:writeExternal()
和readExternal()
-
Serializable
对象的反序列化完全从其存储的字节位里构建,没有调用构造器。而Externalizable
对象反序列化时,会调用公共无参构造器,之后readExternal()
才调用。所以实现这个接口要提供无参构造器 -
Externalizable
对象默认不存储自身的任何字段,因此transient
关键字仅适用于Serializable
对象 -
序列化版本号
serialVersionUID
-
不管那种模式都自行定义版本号
serialVersionUID
-
如果我们不自行定义
serialVersionUID
,JVM 会根据自有的算法帮我们生成一个,这个算法是基于序列化类的字段、JVM 版本等等,这样可能导致即使类文件不变,升级 JDK 小版本都会导致反序列化失败 -
另外,修改方法、transient 字段、静态字段都不影响版本号,这些都不是序列化内容,因此也不是序列化版本号生成的依据
-
JVM 生成的版本号可以通过
jdk/bin/serialver
命令获取,假设想知道某个类在某个 jvm 里的默认序列化版本号,可以用这个命令,来进行迁移改造 -
注意,JSON、XML 等等,和 Java 内置序列化使用的字节流一样,都只是序列化数据的一种协议格式,其序列化机制还是基于 Java 的,并不是对 Java 序列化的一个替代。除了这类有构造和解析包就能直接使用的序列化方案,还有一类需要自己生成各种模板代码(Sub、Skeleton)的序列化方案,比如 rmi、Protocol Buffer 之类的,它们在 Java 侧的序列化一样是基于 Java 原生序列化的
Java 序列化的问题
-
JDK 的开发负责人说序列化是 Java 安全问题之源
Serialization was a horrible mistake in 1997, Some of us tried to fight it, but it went in, and there it is. ...We like to call serialization 'the gift that keeps on giving,' and the type of gift it keeps on giving is security vulnerab