1. 安全序列化的主要最佳實務
序列化就像在機場打包行李:如果你不知道裡面是什麼、也不清楚要把行李交給誰,在安檢時就可能遇到不愉快的驚喜。Java 的序列化能輕鬆保存與還原物件,但若資料來自不可靠的來源,便為各式攻擊大開方便之門。
典型威脅:
Java 中的序列化可能並不安全。若攻擊者注入惡意串流,反序列化時可能出現最糟後果:從修改欄位到執行不期望的程式碼。這不是課本上的嚇人故事——在 Java 的歷史上確實曾出現以此機制為基礎的攻擊。
為什麼會這樣?
關鍵在於,反序列化不只是還原欄位數值。在此過程中會建立一個完整物件:可能呼叫特殊方法(例如 readObject、readResolve),有時還會經由反射觸發程式中的脆弱點。第三方函式庫的類別尤其危險:其中一些在反序列化階段就會執行動作。因此,永遠不要信任來自外部的序列化資料。
對敏感資料使用 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 會被反序列化
此過濾器只允許應用程式中的 User 與 Address 類別。其他一切都會被封鎖——將拋出例外。這可大幅降低惡意物件滲入的風險。
不要從不受信任的來源進行反序列化
黃金法則: 若你不確定資料來源,就不要反序列化。優先選擇在解析時不會執行程式碼的格式(例如 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:忽視資料完整性。 修改序列化檔案的位元組不會被察覺。請使用數位簽章、校驗和或加密。
GO TO FULL VERSION