摘要
介绍序列化、反序列化背景;java实现,以及存在的漏洞和解决方案
一、背景
java序列化将java对象转换为字节流,反序列化根据字节流创建java对象(不通过constructor)。当然,反序列化过程中也会源源不断的产生各种漏洞。
为什么
业务实现需要通过网络传输java对象,或以文件方式将对象保存在磁盘上。
是什么
序列化过程:
java内存中创建的对象,当不再被使用时,会被jvm的垃圾回收器回收。如果想持久化存储java对象,或通过网络传输java对象,需要将对象转换为二进制流。在java中,可通过实现Serializable接口实现序列化功能。
反序列化过程:
从持久化存储二进制流,或网络传输的二进制流,创建出java对象。
二、Java实现机制和实践
(1)实现机制
序列化过程,利用反射机制从java对象中获取对象字段,包括private和final类型字段。如果字段属于对象类型,会递归方式获取对象类型字段中的字段。
反序列化过程中,可能存在安全漏洞。修改二进制流中的数据,或在二进制流数据中插入数据,会导致反序列化对象失败,或字段内容被篡改。
(2)实现代码
待序列化/反序列化对象
public class ValueObject implements Serializable {
private static final long serialVersionUID = 1L;
private String value;
private String sideEffect;
public ValueObject() {
this("empty");
}
public ValueObject(String value) {
this.value = value;
this.sideEffect = java.time.LocalTime.now().toString();
}
}
序列化
private static void writeObject2File() throws IOException {
ValueObject vo1 = new ValueObject("Hi");
FileOutputStream fos = new FileOutputStream("ValueObject.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(vo1);
oos.close();
fos.close();
}
反序列化
private static void readFile2Object() throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream("ValueObject.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
ValueObject vObject = (ValueObject) ois.readObject();
System.out.println(vObject.getValue() + "--" + vObject.getSideEffect());
}
(3)readObject和writeObject
- 类中没有定义私有的writeObject或readObject方法,将调用默认的方法来根据目标类中的属性来进行序列化和反序列化
- 类中定义了私有的writeObject或readObject方法,将调用目标类指定的writeObject或readObject方法来实现
(4)readResolve和writeReplace
- 类中定义readResolve方法会在readObject之后调用,反序列化时readResolve方法覆盖readObject方法的修改
- 类中定义writeReplace()方法,将调用目标writeReplace方法返回值的对象
三、存在的漏洞和最佳实践
反序列化漏洞场景举例:修改二进制序列化文件,会导致反序列化失败,或反序列化对象字段被篡改。
(1)结构化数据传输
JAX.RS框架中,奖Java对象作为Json或xml或其他结构化数据表带格式进行传输,相比于使用序列化、反序列化传输方式,更加简洁、安全。通过@Produce注解表示提供的数据格式。
@Produces(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_JSON)
(2)谨慎实现Serializebale接口
存在继承关系的类,或内部类,尽量不要实现Serializable接口。继承系列类,更可能会变动。
- 类一旦被发布,降低修改该类的灵活性。
- 增加了类被入侵的风险。因为反序列化是隐藏的无约束的“构造器”
- 类新版本被发布后,需兼容测试历史版本的类
- 类中若存在List类型字段是,序列化和反序列化会消耗过多空间、时间,还可能存在栈溢出
(3)不要使用默认序列化UID
Java类实现Serializable接口,若不指定serialVersionUID,JVM会根据变量、方法名、类型、成员属性等自动生成serialVersionUID。后期一旦类被修改,会导致反序列化不兼容。
类字段加入transient关键字,序列化时会略过该字段,反序列化时,对应字段初始化为默认值。即引用为null,boolean为false,数值为0
(4)保护性编写readObject方法
适用于类中某些字段有限制情况,例如类中某些字段值存在上下限。若readObject不加入保护性限制,反序列化可能出现不符合要求的值。
通过在readObject中加入保护性操作,解决上述问题。
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
start = new Date(start.getTime());//拷贝字段值
end = new Date(end.getTime());
if(start.compareTo(end) >0){//检查限制条件
throw new InvalidObjectException(start + " " + end);
}
}
(5)使用序列化代理
序列化代理模式可以解决大多序列化问题。该方式通过一个私有嵌套类表 序列化类的逻辑状态,构造器只从序列化类中复制数据。外部类:使用的类;内部类:代理类
- 序列化过程:外部类调用内部代理类实例返回
- 反序列化:内部类直接调用外部类构造方法返回实例,形成保护
public class Period implements Serializable{
static final long serialVersionUID = 42L;
private final Date start;
private final Date end;
public Period(Date start,Date end){
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
}
// 列化类中含有Object writeReplace()方法,那么实际序列化的对象将是作为writeReplace方法返回值的对象
private Object writeReplace(){
System.out.println("序列化");
return new SerializationProxy(this);//序列化过程,返回代理类对象,外层类不变
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
throw new InvalidObjectException(start + " " + end);
}
//序列化类代理---内部类
private static class SerializationProxy implements Serializable{
static final long serialVersionUID = 42L;
private final Date start;
private final Date end;
SerializationProxy(Period p){
this.start = p.start;
this.end = p.end;
}
private Object readResolve(){//该方法在readObject后执行。反序列化过程,通过Period的构造器校验参数
System.out.println("反序列化");
return new Period(start,end);
}
}
public static void main(String[]args) throws IOException, ClassNotFoundException {
Period period = new Period(new Date(),new Date());
FileOutputStream fos = new FileOutputStream("period.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(period);
oos.close();
fos.close();
FileInputStream fis = new FileInputStream("period.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
Period periodDser = (Period) ois.readObject();
System.out.println(periodDser.start.toString());
}
}
四、反序列化异常
StreamCorruptedException
java序列化文件存在序列化头StreamHeader,包含magic number和version number用于检查文件是否修改。
引用:https://snyk.io/blog/serialization-and-deserialization-in-java/