1. 二進位序列化的安全性
在 Java 中,序列化不僅僅是保存物件的欄位。只要物件實作了介面 Serializable,就能「還原」成任意內容的任意物件。乍看之下很方便!但如果你的應用程式會反序列化來自不受信任來源的資料(例如來自網路或被攻擊者掉包的檔案),它就有成為攻擊受害者的風險。
它是如何運作的?
在反序列化期間,Java 會根據位元組流建立物件,而不會呼叫類別的建構子。若類別實作了像 readObject 之類的特殊方法,它們會被自動呼叫。攻擊者可以「構造」位元組流,使得在反序列化時觸發脆弱的程式碼執行。
範例:「gadget chain」攻擊
試想有個類別在反序列化時會啟動外部指令(讀取檔案或呼叫 shell)。如果攻擊者知道應用程式會反序列化特定型別的物件,他就能投遞特製的位元組流,導致惡意程式碼被執行。這類攻擊通常被組織為「gadget 鏈」(gadget chain)— 一連串呼叫的序列,最終落到危險操作上。
為何如此嚴重?
反序列化是把資料流還原為實際物件的過程,而在這個時刻可能執行任意程式碼。若資料來自不受信任來源,就等於打開了遠端程式碼執行(RCE)的門。許多大型公司已發布建議,避免不安全的反序列化;在現代企業專案中,二進位序列化經常因安全政策而被禁止。
如何防護?
- 切勿反序列化來自不受信任來源的物件。
- 使用 whitelisting — 以白名單明確列出允許反序列化的型別。
- 對外部整合時,優先使用文字格式:JSON、XML。
- 若序列化無可避免,使用支援安全反序列化設定的函式庫(例如可限制型別的 Jackson)。
- 若無法確信其安全性,請限制使用非標準的序列化方法(readObject、readResolve 等)。
2. 類別版本相容性
Java 的二進位序列化與類別結構緊密耦合。在 1.0 版本序列化了物件,接著更新了類別(新增/刪除欄位)— 嘗試把「舊」物件反序列化到新版本時,可能導致錯誤或資料遺失。
Java 如何判定相容性?
這是透過特殊欄位 serialVersionUID 來完成。它是類別的版本識別碼。若序列化資料中的 serialVersionUID 與目前類別的不一致,就會拋出 InvalidClassException,反序列化將不會發生。
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 明確指定版本
private String name;
private int age;
}
如果你修改了類別結構(例如新增欄位 email)卻沒有變更 serialVersionUID,Java 會認為類別相容,並嘗試反序列化舊物件。若未顯式指定 serialVersionUID,JVM 會根據結構自動生成,任何變更都可能導致不相容。
當版本不一致/一致時會發生什麼?
若識別碼不一致 — 反序列化不會發生:InvalidClassException。若一致 — 欄位會依名稱與型別進行對應:新欄位會取得預設值(null、0),被刪除的欄位則會被忽略。若變更欄位的型別或名稱,可能出現錯誤或資料被錯誤解讀。
實用建議。 在可序列化的類別中,務必顯式設定 serialVersionUID。僅在進行不相容的變更時(移除/更改重要欄位的型別)才調整它。新增新欄位時可保留既有識別碼 — JVM 能正確處理舊物件。
表格:變更類別時會發生什麼
| 類別的變更 | 反序列化時會如何? |
|---|---|
| 新增欄位 | 取得預設值(0、null) |
| 移除欄位 | 讀取舊資料時會被忽略 |
| 變更欄位型別 | 丟出例外或產生不正確資料 |
| 變更欄位名稱 | 舊欄位被忽略,新欄位為預設值 |
| 變更 serialVersionUID | 拋出 InvalidClassException |
3. 標準序列化的限制
並非所有物件都能被序列化
具有 transient 與 static 修飾子的欄位不會被序列化。static 是因為它屬於類別而非物件;transient 則表示你已明確禁止序列化該欄位。
有些物件天生不可序列化:Thread、資料庫連線、socket、Scanner 等。如果你的類別包含此類型的欄位且未標記為 transient,就會得到 NotSerializableException。
import java.io.Serializable;
import java.util.Scanner;
public class Session implements Serializable {
private transient Scanner scanner; // 不會被序列化!
private String login;
}
效能與擴充性的問題
序列化大型物件圖可能很慢,且對記憶體需求很高。
二進位格式不利於與其他平台與語言整合 — 基本只有 Java 能「理解」它。
很難精準控制實際被序列化的內容,尤其在深層繼承階層與循環參照的情況下。
維護舊資料的問題
長期保存二進位快照很有風險。過了一兩年類別結構更動,舊檔案往往就載不回來。
真實案例:「我們在三年前保存了使用者快取的序列化檔,更新應用後,現在載不回來了。你好啊,遺失的資料!」
4. Best practices:如何避開陷阱
- 僅在你能掌控序列化與反序列化雙方的內部情境中使用二進位序列化。
- 不要將二進位序列化用於對外整合與重要資料的長期保存。
- 在可序列化的類別中,務必顯式指定 serialVersionUID。
- 將不該被序列化的欄位標記為 transient。
- 與外部系統交換資料時,使用文字格式與現代函式庫:JSON、XML、Jackson、Gson、JAXB。
- 為了相容性請做版本控管:在類別中保存版本,並在反序列化時調整處理邏輯。
- 若序列化僅用於快取,無需不計代價維持相容性:快取可以重新計算。
- 不要在可序列化物件中存放敏感資料(密碼、金鑰)— 序列化並不會加密資料。
5. 使用二進位序列化的常見錯誤
錯誤 №1:從不受信任來源反序列化資料。 最危險的錯誤 — 接受並反序列化來自「外部」的物件(來自網路、使用者、或被掉包的檔案)。這是通往包含 RCE 在內嚴重弱點的捷徑。
錯誤 №2:未更新 serialVersionUID 就隱性改動類別結構。 如果未顯式指定識別碼,JVM 會自動生成。任何結構變更(甚至欄位順序)都會導致不相容,舊物件將無法載入。
錯誤 №3:嘗試序列化含有不可序列化欄位的物件。 如果類別中有欄位未實作 Serializable,且未標記為 transient,序列化將以例外結束。
錯誤 №4:在可序列化物件中保存暫時性或敏感資料。 Token、密碼、資源的暫時描述元 — 這些都可能不小心落到檔案中。
錯誤 №5:將二進位序列化用於長期保存與跨版本交換。 類別首次更新後,就很可能出現「壞掉」的資料與相容性問題。
錯誤 №6:以為 static 與 transient 欄位會在反序列化後「恢復」。 這些欄位不會被序列化;載入後它們會是預設值。
GO TO FULL VERSION