第七十六条 保护性编写readObject方法

书中本条目开头给了一个代码例子。

public final class Period implements Serializable {
    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());
        if (this.start.compareTo(this.end) > 0)
            throw new IllegalArgumentException(
                    start + " after " + end);
    }
    public Date start () { return new Date(start.getTime()); }
    public Date end () { return new Date(end.getTime()); }
    public String toString() { return start + " - " + end; }

}

public class BogusPeriod {
    
    private static final byte[] serializedForm = new byte[] {
            (byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,
            0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8,
            0x2b, 0x4f, 0x46, (byte)0xc0, (byte)0xf4, 0x02, 0x00, 0x02,
            0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c,
            0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f,
            0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74,
            0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70,
            0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75,
            0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a,
            (byte)0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00,
            0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte)0xdf,
            0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03,
            0x77, 0x08, 0x00, 0x00, 0x00, (byte)0xd5, 0x17, 0x69, 0x22,
            0x00, 0x78 };
    public static void main(String[] args) {
        Period p = (Period) deserialize(serializedForm);
        System.out.println(p);
    }
    
    private static Object deserialize(byte[] sf) {
        try {
            InputStream is = new ByteArrayInputStream(sf);
            ObjectInputStream ois = new ObjectInputStream(is);
            return ois.readObject();
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }
}

什么意思呢? 我们从上一条中知道,对象的序列化与反序列化的流程,序列化时把对象通过 ObjectOutputStream 的 writeObject 方法,转换为二进制写入文件或磁盘,对应的反序列化则是通过 ObjectInputStream 的 readObject 方法,把文件中的二进制转换为对象,那么,ObjectInputStream 是流,需要传入二进制的内容,按照通常理解,需要路径获取文件,然后转换为流,ObjectInputStream 的构造方法接收流,我们可以把文件转换为流,当然也可以把字节数组转换为流。上一条中用的是 FileInputStream 找到文本,包装为流,本条中上面的例子,则是用 ByteArrayInputStream 把字节数组包装为流,内容有了,就可以反序列化了。

我们假设上面的字节数组的数据就是 Period 对象的序列化后对应的内容,那么就可以通过字符的形式来反序列化,这就证明了可以从文本中读取数据进行反序列化,也可以通过按照规则生成的字节数组中进行反序列化,此时,按照数中所说,运行代码后,打印的结果为 "Fri Jan 01 12:00:00 PST 1999 - Sun Jan 01 12:00:00 PST 1984"。 很明显,我们的 Period 对象的构造方法中有时间的约束,但明显此时违反了约束,仍能生成对象。这就说明了反序列化实际上还对应着一个隐形的构造方法,可以不受约束。如果有心人篡改信息来攻击我们的代码,怎么办?

我们知道,反序列化时要执行 readObject 方法,同时我们自定义序列化时也提到了对象中编写 readObject 方法,结决方法出现了

        private void readObject(ObjectInputStream s)
                throws IOException, ClassNotFoundException {
            s.defaultReadObject();
            if (start.compareTo(end) > 0)
                throw new InvalidObjectException(start +" after "+ end);
        }

反序列化会执行这个方法,所以,在执行 s.defaultReadObject(); 后获取对象的属性值,然后做校验,不符合情况时就抛出异常,就解决上面的问题了。

上面的例子是通过整体改变数据源来攻击,但还隐藏着一个小问题,如果是内部通过关联对象来修改属性,上面的方法就防不住了。

public class MutablePeriod {
    public final Period period;
    public final Date start;
    public final Date end;

    public MutablePeriod() {

        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            out.writeObject(new Period(new Date(), new Date()));

            byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // Ref #5
            bos.write(ref); // The start field
            ref[4] = 4; // Ref # 4
            bos.write(ref); // The end field
            ObjectInputStream in = new ObjectInputStream(
                    new ByteArrayInputStream(bos.toByteArray()));
            period = (Period) in.readObject();
            start = (Date) in.readObject();
            end = (Date) in.readObject();
        } catch (Exception e) {
            throw new AssertionError(e);
        }

    }

}

    private static void test() {
        MutablePeriod mp = new MutablePeriod();
        Period p = mp.period;
        Date pEnd = mp.end;
        // Let's turn back the clock
        pEnd.setYear(78);
        System.out.println(p);
        // Bring back the 60s!
        pEnd.setYear(69);
        System.out.println(p);
    }


这段代码的意思是 MutablePeriod 对象中 包含了三个属性,Period 和 Date ,本来毫不相关的三个属性,但是,在序列化时,存入 Period 中的两个属性的时间是当前时间,然后我们通过序列化数据时,人为的添加字节数组,加入一段数据,

        byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // Ref #5
                bos.write(ref); // The start field
                ref[4] = 4; // Ref # 4
                bos.write(ref); // The end field
使 MutablePeriod 对象中的 Date start; Date end; 与 Period 对象中的 Date start; Date end; 建立起关系,使MutablePeriod中的两个Date引用指向Period中的Date域,这就是“恶意编制的对象引用”,这样,只要改变了 MutablePeriod 中 Date 的值,也就是改变了 Period 中 Date 的值。所以上述代码打印出来的值为
Tue Dec 25 11:28:27 CST 2018 - Mon Dec 25 11:28:27 CST 1978
Tue Dec 25 11:28:27 CST 2018 - Thu Dec 25 11:28:27 CST 1969
这个明显的,也违反了 Period 的构造条件,end 的时间居然比 start 的时间早。
对于上述问题,怎么解决呢?既然外面通过恶意的对象引用,我们没办法去把它们的引用去掉,那么只能在自己的代码中加防护了,反序列化时,会用到 readObject 方法,我们在这个里面,通过保护性clone方法来结决,如下

        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 + " after " + end);
        }
我们重新获取 start 和 end 的时间,然后再次创建一个对象,保护性的clone,这样,引用就被切断了,不会被外部攻击了。但这样修改后,就需要把成员变量的 start 和 end  的final 修饰的关键字去掉了。这样,再次运行,结果明显是当前时间,不受外部影响。

Tue Dec 25 11:54:55 CST 2018 - Tue Dec 25 11:54:55 CST 2018
Tue Dec 25 11:54:55 CST 2018 - Tue Dec 25 11:54:55 CST 2018
 

你可能感兴趣的:(java,effective,注解,序列化,readObject,自定义序列化,保护性clone)