JAVA序列、反序列化及漏洞

摘要

介绍序列化、反序列化背景;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/

你可能感兴趣的:(java)