1. 程式人生 > >第七十六條 保護性編寫readObject方法

第七十六條 保護性編寫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