把收到的位元組還原成物件之所以危險,並不是因為內容格式錯誤。而是在某些格式中,外部資料可以決定要建立哪種型別的物件。
為什麼輸入驗證來得太晚
純資料格式(JSON/XML)
外部資料只決定值 → 你接收後,再對應到自己的型別 → 驗證有效
會還原物件的格式
外部資料決定值和型別 → 還原時執行屬於所選型別的程式碼 → 執行流程根本到不了你的檢查
多數輸入驗證問的是「這個值可以接受嗎」。但在會還原物件的格式中,還原本身(要實例化哪個類別、過程中執行什麼程式碼)是由外部資料操控的。在你的驗證被呼叫之前,某些東西可能已經執行完畢了。
這和原型污染是同樣的形態:外部資料決定的是結構,而不是值。名稱不同、語言不同,防禦的思路相同。
會出什麼問題(OWASP 列出的三項)
- 阻斷服務
- 讓還原本身變得極為耗費資源,或讓它失敗,使應用程式停止
- 存取控制繞過
- 代表權限或角色的物件,被建構成對攻擊者有利的樣子
- 遠端程式碼執行
- 串接還原過程中被呼叫的處理程序,達到任意程式碼執行,這是最嚴重的結果
「我們的框架不會這樣」不是安全的前提
這不只是舊語言的問題。我們統計 NVD 資料中的 Next.js 漏洞時,反序列化(CWE-502)在 CWE 分布中排名很高,因為現代框架也帶有反序列化請求內容的路徑,Server Actions 就是其中之一(Next.js 和 React 的漏洞很多嗎?)。「不是我寫的」不代表「它不存在」。
兩種防禦
1. 不讓任何東西選擇型別(第一選擇)
OWASP 的說法:「改用 JSON 或 XML 這類純資料格式,可以降低自訂反序列化邏輯被挪作惡意用途的機會。」
以只攜帶值的格式接收外部輸入,再用自己的程式碼對應到自己的型別。光是這樣,攻擊者就無法選擇要建立什麼。在多數程式碼中,這就能解決問題。
2. 加上簽章,並拒絕沒有簽章的
無法避免使用自訂格式時,指南指出「可以在序列化過程中為它們加上簽章」,然後「選擇不反序列化任何沒有經過驗證簽章的訊息」。
重點是在還原之前驗證。基於上述理由,先還原再檢查可能已經太遲了。順序就是控制的全部。
有些機制無法靠設定變得安全
關於 .NET 的 BinaryFormatter,OWASP 明確表示「BinaryFormatter 型別是危險的,而且無法變得安全」。
這句話在實務上很重要:確實存在「只要正確使用就沒問題」根本不成立的工具。強化選項、增加驗證、撰寫允許清單,都無法讓它變得安全。唯一的控制就是不使用它。
所以在稽核時,首先要把設定能保護的和不能保護的分開。把力氣花在後者不是緩解,而是拖延。
OWASP 依語言點名的項目
- PHP
- 避免使用
unserialize();優先使用 JSON - Python
pickle/c_pickle、PyYAML 的load和jsonpickle被列為危險- Java
- 覆寫
ObjectInputStream#resolveClass(),限制可以還原的類別 - .NET
BinaryFormatter是危險的,而且無法變得安全,不要使用
共同點是:語言內建的方便持久化機制,正是絕不能面對外部資料的東西。它是為了在彼此信任的雙方之間搬運整個物件而設計的,並沒有假設傳送者懷有敵意。
各語言安全與不安全的 API
下表整理了各語言官方文件警告不要用在外部資料上的東西,以及建議改用的東西。如果左欄的東西出現在你的程式碼中,請查清楚它的資料來自哪裡。
| 語言 | 不要用在外部資料上 | 改用 | 官方文件的說法 |
|---|---|---|---|
| Python | pickle.load/pickle.loads、marshal、yaml.load(使用 Loader=yaml.Loader 時)和 yaml.unsafe_load、jsonpickle.decode | json.loads、yaml.safe_load | pickle「不安全」;需要偵測竄改時用 hmac 簽章 |
| PHP | unserialize()(即使設定了 allowed_classes) | json_decode()/json_encode() | 不論 allowed_classes 如何,都不要傳入不可信任的輸入 |
| Ruby | Marshal.load | JSON,或其他只載入基本型別的格式 | 從不可信任的來源載入可能導致遠端程式碼執行 |
| Java | 對外部資料使用 ObjectInputStream#readObject、XMLDecoder、XStream#fromXML | 把 JSON 對應到自己的類別(DTO) | 無法避免時,用 ObjectInputFilter 限制允許的類別 |
| .NET | BinaryFormatter、SoapFormatter、NetDataContractSerializer、LosFormatter、ObjectStateFormatter、Json.NET 的 TypeNameHandling 設為 None 以外的值 | System.Text.Json、XmlSerializer、DataContractSerializer | 從 .NET 9 起,使用 BinaryFormatter 會擲出例外 |
並排比較,差別如下。不安全的版本把收到的東西直接變回物件;安全的版本把它當成值讀取,只把需要的欄位移到自己的型別中。
# 避免:把收到的位元組直接變回物件
order = pickle.loads(request_body)
# 改用:讀取值,再只把需要的欄位移到自己的型別
data = json.loads(request_body)
order = Order(id=int(data["id"]), quantity=int(data["quantity"]))在安全的版本中,由你的程式碼(Order)決定要建立哪個類別。傳入的資料只決定 id 和 quantity 的值,而一般的輸入驗證對這些值是有效的。
在自己的程式碼中要檢查什麼
列出外部資料最先到達的所有地方
請求內容、Cookie、工作階段、佇列、上傳、Webhook、快取:所有先被儲存、之後再被還原的地方。工作階段和快取最容易漏掉:你以為是自己的資料,依路徑不同也可能從外部到達。
找出其中使用會還原型別的格式的地方
搜尋上面清單中點名的函式和類別。這些是最先要移除的東西。如果正在使用無法變得安全的機制(BinaryFormatter 等),那就是最優先事項,除了遷移之外沒有其他解法。
改用純資料格式
接收 JSON 或 XML,並用自己的程式碼對應到自己的型別。在對應時加上結構描述(schema)驗證,就能同時擋下非預期的鍵(與原型污染的對策相同)。
無法改用時,用簽章保護
如果格式無法改變,就在序列化時加上簽章,並在還原前驗證。把**「沒有簽章就不還原」**設為預設。金鑰的處理方式請參考.env 和 API 金鑰危險在哪裡,絕不要和程式碼放在同一個地方。
清點相依套件所做的還原
即使你自己一行都沒寫,相依套件也可能在還原物件。被歸類為 CWE-502 的 CVE 不斷出現,所以請交給機器監控(osv-scanner 入門/實用的 CVE 修補手冊)。
如何檢查自己的環境
以下把上述步驟中「找出來」的部分,轉換成具體的指令和設定。所有檢查都只讀取你自己的儲存庫和伺服器。
文字搜尋原始碼
先列出所有候選位置。使用 ripgrep(rg)時,搜尋方式如下(grep -rnE 也可以使用相同的模式)。
# Python
rg -n "pickle\.loads?\(|marshal\.loads?\(|yaml\.load\(|yaml\.unsafe_load|jsonpickle\.decode"
# PHP
rg -n "unserialize\("
# Ruby
rg -n "Marshal\.load"
# Java
rg -n "ObjectInputStream|readObject\(|XMLDecoder|fromXML\("
# .NET
rg -n "BinaryFormatter|SoapFormatter|NetDataContractSerializer|LosFormatter|ObjectStateFormatter|TypeNameHandling"並不是每個搜尋結果都危險。對每一個結果,寫下它接收的資料來自哪裡,並優先處理與外部可到達的路徑相連的部分:請求、Cookie、上傳、共用儲存空間。
尋找被壓下的警告
在 .NET 中,使用 BinaryFormatter 會產生 SYSLIB0011 警告或錯誤。壓下這個警告的設定,就標示出危險呼叫仍留在程式碼中的地方。
rg -n "SYSLIB0011|CA23[0-3][0-9]" --glob "*.cs" --glob "*.csproj" --glob ".editorconfig" --glob "*.props"如果找到 #pragma warning disable SYSLIB0011,或 <NoWarn> 中有這個 ID,請確認它為什麼在那裡,以及何時會遷移。
啟用靜態分析規則
Python 相關的 Bandit 檢查是 B301(pickle 等)、B302(marshal)和 B506(yaml.load);bandit -r . -t B301,B302,B506 只會執行這三項。.NET 相關的程式碼分析規則是 CA2300 系列(例如會標示 Json.NET 的 TypeNameHandling 的 CA2326)。即使在 .NET 10 中 CA2326 預設也沒有啟用,所以請在 .editorconfig 中加入 dotnet_diagnostic.CA2326.severity = warning 之類的一行來開啟。在 CI 中執行這些檢查,新加入的呼叫也會被擋下。
Java 要檢查執行時期的篩選設定
Java 可以在執行時期限制反序列化能建立哪些類別(序列化篩選器)。這個機制在 JDK 9(JEP 290)加入,並在 Java 17(JEP 415)可以依情境切換。對於執行中的程序,請在 jcmd <PID> VM.system_properties 的輸出中尋找 jdk.serialFilter。它也可以在 java.security 檔案中設為安全性屬性,所以兩處都要檢查。如果兩處都沒有,而你的程式碼也沒有呼叫 ObjectInputFilter.Config.setSerialFilter,就代表沒有整個程序層級的篩選器。
檢查相依套件的已知漏洞
即使你自己的程式碼中沒有這類呼叫,相依套件也可能在還原物件。用 osv-scanner 或類似工具列出相依套件的已知漏洞,並優先更新被歸類為 CWE-502(不可信任資料的反序列化)的漏洞。
常見的錯誤與修正方法
| 錯誤 | 為什麼有問題 | 修正方法 |
|---|---|---|
| 因為「是內部的」或「這個檔案是我自己存的」就信任資料 | Microsoft 舉出了資料在開發者沒注意到的情況下跨越信任邊界的例子:共用的存檔、雲端同步、遭入侵的員工電腦 | 只要有任何路徑能從外部到達,就當成外部資料處理。不還原任何沒有簽章的東西 |
| 一個一個把危險的類別加進封鎖清單 | 對清單以外或尚未發現的組合毫無作用。PHP 手冊指出即使設定了 allowed_classes 也不要傳入不可信任的輸入 | 把格式改成 JSON 等。只有在無法避免時,才列出允許的類別 |
| 先還原,再驗證內容 | 還原時就會執行程式碼,所以驗證來得太晚 | 把簽章驗證放在還原之前 |
| 壓下警告就當成已修正 | 讓 SYSLIB0011 或 CA2326 不再出現,危險的呼叫仍然存在 | 搜尋被壓下的警告,並訂出遷移期限 |
| 以為「是 JSON 所以安全」 | 讓 JSON 自己指定型別的設定(例如 Json.NET 的 TypeNameHandling)會把它變成會選擇型別的格式 | 讓 TypeNameHandling 維持 None(預設值) |
在 PyYAML 中使用 yaml.load | PyYAML 的文件說 yaml.load 和 pickle 一樣強大,可能呼叫任何 Python 函式 | 換成 yaml.safe_load |
真實案例:擴充套件內的還原
本站關於 CVE-2026-45247 的警示,介紹了 Magento 2 擴充套件「Mirasvit Full Page Cache Warmer」1.11.12 之前版本中的 PHP 物件注入(CWE-502)。它可以在不經驗證的情況下遠端執行程式碼,CVSS 分數為 9.3,並被列入 CISA 的已知遭利用漏洞目錄(KEV)。
教訓是,還原的程式碼不是網站營運者寫的,而是在他們安裝的擴充套件裡。這就是上面「檢查相依套件」步驟重要的原因。修正方法是更新擴充套件,而搜尋自己的程式碼是找不到它的。
資料來源(一手資料)
- OWASP Cheat Sheet Series, "Deserialization Cheat Sheet" — cheatsheetseries.owasp.org(三項影響、改用純資料格式、為完整性加上簽章、各語言的注意事項,以及關於
BinaryFormatter的說明,皆出自這份文件;2026 年 9 月 5 日確認) - Python documentation, "pickle" — docs.python.org(「不安全」的警告;用
hmac簽章、優先使用 JSON) - PyYAML Documentation — pyyaml.org(
yaml.load與yaml.safe_load的差別) - PHP Manual, "unserialize" — php.net(不論
allowed_classes如何都不要傳入不可信任的輸入;使用 JSON) - Ruby documentation, "Marshal" — docs.ruby-lang.org
- Microsoft Learn, "Deserialization risks in use of BinaryFormatter and related types" — learn.microsoft.com(同樣危險的型別、建議的替代方案、.NET 9 起的行為、跨越信任邊界的例子)
- Microsoft Learn, "SYSLIB0011" and "CA2326" — SYSLIB0011/CA2326
- OpenJDK, "JEP 290: Filter Incoming Serialization Data" and "JEP 415: Context-Specific Deserialization Filters" — JEP 290/JEP 415
- Bandit documentation (B301, B302, B506) — bandit.readthedocs.io
- 語言對照表、搜尋步驟和常見錯誤,已於 2026 年 10 月 6 日依上述文件確認
延伸閱讀
- 同樣的形態:Node.js 的原型污染(外部資料決定結構)/檔案上傳漏洞(外部資料決定放在哪裡)
- 用語:遠端程式碼執行(RCE)是什麼
- 真實案例:CVE-2026-45247(Magento 擴充套件中的 PHP 物件注入)
- 依框架:ASP.NET Core/Spring Boot/Django 的安全性
- 相依套件:osv-scanner 入門/實用的 CVE 修補手冊
FAQ
Q反序列化為什麼這麼危險?不就是資料轉換嗎?
因為在某些格式中,它不是資料轉換,而是物件的重建。當「要建立哪個類別」的資訊夾帶在資料裡時,攻擊者就能選擇你要建立什麼。OWASP 表示,已發現對反序列化器的攻擊可以造成阻斷服務、存取控制繞過或遠端程式碼執行攻擊。難以防禦的原因在於,攻擊在你的值檢查執行之前就已經成功了。
Q最可靠的防禦是什麼?
把來自外部的資料限制在無法選擇型別的格式。OWASP 的說法是:改用 JSON 或 XML 這類純資料格式,可以降低自訂反序列化邏輯被挪作惡意用途的機會。如果無法避免使用自訂格式,同一份指南指出,可以在序列化過程中為訊息加上簽章,並選擇不反序列化任何沒有經過驗證簽章的訊息。先改變格式;做不到時,再用簽章加以約束。
Q可以靠強化設定讓它變得安全嗎?
取決於機制。OWASP 明確表示,.NET 的 BinaryFormatter 型別是危險的,而且無法變得安全。有些機制不會因為設定或額外的驗證而變得安全,唯一的解決方法是停止使用。請把設定能保護的和不能保護的分開。確實存在「只要正確使用就沒問題」這句話不成立的工具。
Q在我使用的語言中該注意什麼?
OWASP 直接點名的包括:PHP 避免使用 unserialize(),優先使用 JSON;Python 的 pickle/c_pickle、PyYAML 的 load 和 jsonpickle 是危險的;Java 要覆寫 ObjectInputStream#resolveClass() 來限制可以還原的類別;.NET 不要使用 BinaryFormatter。共同點是,語言內建的方便持久化機制,正是絕不能用在外部資料上的東西。
Q如何在自己的程式碼中找出不安全的反序列化?
分三步。首先,用 ripgrep 或 grep 文字搜尋 pickle.loads、unserialize、Marshal.load、ObjectInputStream 和 BinaryFormatter 等呼叫。其次,啟用靜態分析規則(Python 用 Bandit 的 B301、B302 和 B506;.NET 用 CA2300 系列的程式碼分析規則)。第三,用 osv-scanner 等工具掃描相依套件的已知漏洞,優先更新 CWE-502 的漏洞。對每個找到的呼叫,寫下它接收的資料來自哪裡,並優先修正從外部可以到達的那些。
Q只要用 JSON 就一定安全嗎?
如果你啟用了讓資料自己指定型別的設定,就不安全。常見的例子是 .NET 中 Json.NET 的 TypeNameHandling;Microsoft 的程式碼分析規則 CA2326 指出不要使用 None 以外的任何值。JSON 的好處是它只攜帶值,所以由接收端的程式碼決定要建立哪個類別。請確認你沒有開啟會抵銷這個好處的設定。