问题
由于反序列化也可以作为一种构造对象的方式,但是往往因为这种利用语言之外的机制来创建,而不是普通的构造器,就容易遭受攻击。在反序列化时,实例的创建是由readObject方法来完成的。由于这是一个不同于构造函数的创建类实例的通道,因此在构造函数中的状态约束条件在readObjetc中也得一条不落下的实现。而且readObject的出现,让伪字节流攻击和内部域的盗用攻击成为可能。伪字节流攻击就是伪造一个字节流,通过readObject读取,内部域的盗用攻击是指用一个外部类包含该类,用外部类的字段去指向该类的可变对象。那么应该使用哪种方式,能够极大的减少风险?
答案
要确保能够安全的进行反序列化构造对象,可以采用序列化代理模式。所谓序列化代理模式相当简单,首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个嵌套类被称作序列化代理(serialization proxy),它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只从它的参数中复制数据:它不需要进行任何一致性检查或者保护性拷贝。按设计,序列代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列代理都必须声明实现Serializable接口。例如,下面这种形式:
public class Period implements Serializable{ private static final long serialVersionUID = 1L; private final Date start; private final Date end; public Period(Date start, Date end) { if(null == start || null == end || start.after(end)){ throw new IllegalArgumentException("请传入正确的时间区间!"); } this.start = start; this.end = end; } public Date start(){ return new Date(start.getTime()); } public Date end(){ return new Date(end.getTime()); } @Override public String toString(){ return "起始时间:" + start + " , 结束时间:" + end; } /** * 序列化外围类时,通过这个方法,最后其实是序列化了一个内部的代理类对象! * @return */ private Object writeReplace(){ System.out.println("进入writeReplace()方法!"); return new SerializabtionProxy(this); } /** * 如果攻击者伪造了一个字节码文件,然后来反序列化也无法成功,因为外围类的readObject方法直接抛异常! * @param ois * @throws InvalidObjectException */ private void readObject(ObjectInputStream ois) throws InvalidObjectException{ throw new InvalidObjectException("Proxy required!"); } private static class SerializabtionProxy implements Serializable{ private static final long serialVersionUID = 1L; private final Date start; private final Date end; SerializabtionProxy(Period p){ this.start = p.start; this.end = p.end; } /** * 反序列化这个类时,虚拟机会调用这个方法,最后返回的对象是一个Period对象!这里同样调用了Period的构造函数,会和原来的实例对象一样 */ private Object readResolve(){ System.out.println("进入readResolve()方法,将返回Period对象!"); // 这里进行保护性拷贝! return new Period(new Date(start.getTime()), new Date(end.getTime())); } }
有这样几点需要注意:
- 需要将writeReplace方法添加到外围类中,该方法可以产生一个序列化代理对象来代替外围类的实例。换句话说,writeReplace方法在序列化之前,将外围类的实例转换成了它的代理对象;
- 在外围类要通过readObject方法中抛出异常,即表明禁止调用者调用readObject,杜绝攻击者企图通过readObject进行伪字节流攻击和内部域盗用攻击;
- 内部类中的readResolve方法是利用类本身的构造器和静态工厂去创建对象,这样就避免了为了安全反序列化而遵守类的约束条件而产生的大量代码。
结论
每当写readObject或者writeObject方法的时候,就应该考虑使用序列化代理模式。要想稳健地将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法。