CodeGym /課程 /JAVA 25 SELF /序列化安全:最佳實務

序列化安全:最佳實務

JAVA 25 SELF
等級 43 , 課堂 4
開放

1. 安全序列化的主要最佳實務

序列化就像在機場打包行李:如果你不知道裡面是什麼、也不清楚要把行李交給誰,在安檢時就可能遇到不愉快的驚喜。Java 的序列化能輕鬆保存與還原物件,但若資料來自不可靠的來源,便為各式攻擊大開方便之門。

典型威脅:

Java 中的序列化可能並不安全。若攻擊者注入惡意串流,反序列化時可能出現最糟後果:從修改欄位到執行不期望的程式碼。這不是課本上的嚇人故事——在 Java 的歷史上確實曾出現以此機制為基礎的攻擊。

為什麼會這樣?

關鍵在於,反序列化不只是還原欄位數值。在此過程中會建立一個完整物件:可能呼叫特殊方法(例如 readObjectreadResolve),有時還會經由反射觸發程式中的脆弱點。第三方函式庫的類別尤其危險:其中一些在反序列化階段就會執行動作。因此,永遠不要信任來自外部的序列化資料。

對敏感資料使用 transient

如果類別中有儲存密碼、權杖、私鑰或其他敏感資訊的欄位,請將它們宣告為 transient。這些資料不會出現在序列化串流中。

import java.io.Serializable;

public class User implements Serializable {
    private String username;
    private transient String password; // 不會被序列化

    // ...建構子、getter、setter...
}

反序列化時會發生什麼? 欄位 password 會是預設值(對字串而言為 null)。這很理想:密碼不會被寫入檔案或透過網路傳輸。

明確定義 serialVersionUID

務必明確指定 serialVersionUID。這可降低相容性錯誤的機率,並將反序列化期間類別被替換的風險降到最低。

private static final long serialVersionUID = 1L;

為什麼這對安全很重要? 如果未指定 serialVersionUID,編譯器會根據類別結構自動產生。這可能導致非預期的不相符,理論上也可能被濫用,以相同名稱但不同結構的類別進行置換。

在反序列化後檢查物件型別

不要信任來自網路或檔案的資料。反序列化後,務必在使用之前檢查取得的物件是否為預期型別。

Object obj = objectInputStream.readObject();
if (obj instanceof User) {
    User user = (User) obj;
    // 可安全地處理 user
} else {
    // 非預期的型別 — 擲出例外或處理錯誤
}

為何需要這樣做? 惡意串流可能包含實作了 Serializable 但不符合你的商業邏輯的其他類別的物件。

限制可被反序列化的類別(ObjectInputFilter

自 Java 9 起,使用過濾器——ObjectInputFilter,來限制允許反序列化的類別集合。就像入口的門禁管制。

範例:設定過濾器

import java.io.*;

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
        "com.example.User;com.example.Address;!*"
);

ObjectInputStream in = new ObjectInputStream(inputStream);
in.setObjectInputFilter(filter);

Object obj = in.readObject(); // 現在只有 User 與 Address 會被反序列化

此過濾器只允許應用程式中的 UserAddress 類別。其他一切都會被封鎖——將拋出例外。這可大幅降低惡意物件滲入的風險。

不要從不受信任的來源進行反序列化

黃金法則: 若你不確定資料來源,就不要反序列化。優先選擇在解析時不會執行程式碼的格式(例如 JSON、使用安全剖析器的 XML)。

反例(不佳實務):

// 千萬不要對來自網際網路的資料這樣做!
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object obj = in.readObject(); // 危險!

更好的作法是:

  • 使用 JSON 解析器(例如 Gson/Jackson)或具驗證功能的 XML 解析器。
  • 若必須使用二進位序列化,請透過 ObjectInputFilter 過濾類別,並以 instanceof 檢查型別。

與外部系統交換資料時使用替代格式

在整合場景中,採用在剖析時不會執行程式碼的格式:JSON、XML、Protocol Buffers 等。這幾乎可以杜絕透過反序列化的攻擊。

// 請使用 JSON 解析器,而不是 ObjectInputStream
User user = gson.fromJson(jsonString, User.class);

不要將序列化物件存放在公用位置

包含序列化物件的檔案可能含有敏感資料。不要將它們存放在公用目錄,並在檔案系統層級限制存取權。

不要依賴序列化來保證完整性

序列化並不保證資料完整性或真偽。若不可接受被修改,請使用數位簽章、雜湊校驗或加密。

2. 實作:ObjectInputFilter 範例與脆弱性示範

類別過濾範例

假設我們有一個 User 類別:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;
    private transient String password;

    // ...建構子、getter、setter...
}

過濾器只允許 User

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
        "com.example.User;!*"
);
in.setObjectInputFilter(filter);

現在,如果有人試圖塞入其他類別的物件,反序列化會以錯誤終止。

潛在脆弱性示範

惡意類別:

// 假設有人塞入了這樣的類別
public class Evil implements java.io.Serializable {
    static {
        System.out.println("惡意代碼已執行!");
        // 這裡可能什麼都有……
    }
}

若未過濾類別,反序列化時可能會建立物件 Evil,而在載入類別時靜態初始區塊就會被執行——這已是實際可行的攻擊。

4. 確保序列化安全時的常見錯誤

錯誤 1:反序列化未進行過濾與型別檢查。 開發人員常常從串流讀出物件後就直接轉型為需要的型別。這會為攻擊打開大門。請使用 ObjectInputFilter,並透過 instanceof 檢查型別。

錯誤 2:將敏感資料未標註為 transient 若忘了將密碼/金鑰宣告為 transient,它們會進入串流,並可能隨檔案外洩。

錯誤 3:未指定 serialVersionUID 沒有明確的 serialVersionUID,可能出現非預期的相容性錯誤,以及與類別置換相關的風險。

錯誤 4:用序列化與外部系統交換資料。 二進位序列化在應用內(例如快取)很方便,但對外交換很危險。優先選擇 JSON/XML/Proto 搭配安全的剖析器。

錯誤 5:忽視資料完整性。 修改序列化檔案的位元組不會被察覺。請使用數位簽章、校驗和或加密。

1
問卷/小測驗
序列化設定,等級 43,課堂 4
未開放
序列化設定
序列化設定
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION