JAVA序列、反序列化及漏洞

摘要

介绍序列化、反序列化背景;java实现,以及存在的漏洞和解决方案

一、背景

java序列化将java对象转换为字节流,反序列化根据字节流创建java对象(不通过constructor)。当然,反序列化过程中也会源源不断的产生各种漏洞。

为什么

业务实现需要通过网络传输java对象,或以文件方式将对象保存在磁盘上。

是什么

序列化过程:

java内存中创建的对象,当不再被使用时,会被jvm的垃圾回收器回收。如果想持久化存储java对象,或通过网络传输java对象,需要将对象转换为二进制流。在java中,可通过实现Serializable接口实现序列化功能。

反序列化过程:

从持久化存储二进制流,或网络传输的二进制流,创建出java对象。

二、Java实现机制和实践

(1)实现机制

序列化过程 ,利用反射机制从java对象中获取对象字段,包括private和final类型字段。如果字段属于对象类型,会递归方式获取对象类型字段中的字段。
反序列化过程中,可能存在安全漏洞。修改二进制流中的数据,或在二进制流数据中插入数据,会导致反序列化对象失败,或字段内容被篡改。

(2)实现代码

待序列化/反序列化对象

java 复制代码
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();
    }
}

序列化

java 复制代码
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();
    }

反序列化

java 复制代码
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注解表示提供的数据格式。

java 复制代码
@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中加入保护性操作,解决上述问题。

java 复制代码
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)使用序列化代理

序列化代理模式可以解决大多序列化问题。该方式通过一个私有嵌套类表 序列化类的逻辑状态,构造器只从序列化类中复制数据。外部类:使用的类;内部类:代理类

  • 序列化过程:外部类调用内部代理类实例返回
  • 反序列化:内部类直接调用外部类构造方法返回实例,形成保护
java 复制代码
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/

相关推荐
坐吃山猪15 小时前
SpringBoot01-配置文件
java·开发语言
我叫汪枫15 小时前
《Java餐厅的待客之道:BIO, NIO, AIO三种服务模式的进化》
java·开发语言·nio
yaoxtao15 小时前
java.nio.file.InvalidPathException异常
java·linux·ubuntu
Swift社区17 小时前
从 JDK 1.8 切换到 JDK 21 时遇到 NoProviderFoundException 该如何解决?
java·开发语言
DKPT18 小时前
JVM中如何调优新生代和老生代?
java·jvm·笔记·学习·spring
phltxy18 小时前
JVM——Java虚拟机学习
java·jvm·学习
seabirdssss19 小时前
使用Spring Boot DevTools快速重启功能
java·spring boot·后端
喂完待续19 小时前
【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
java·spring·spring cloud·云原生·架构·big data·序列晋升
benben04419 小时前
ReAct模式解读
java·ai