libexpat 未擋孤立高代理項,畸形 UTF-16 混入 XML 解析結果
來源:oss-security mailing list(Sebastian Pipping)· 2026-09-22
當一份 XML 文件裡藏著沒有配對低代理項的高代理項時,libexpat 在 2.8.5 之前不會擋下它,只會把這段畸形UTF-16 原樣往下丟給呼叫端,讓解析器與後續處理程式對同一份輸入產生不同的解讀。libexpat 維護者 Sebastian Pipping 在 oss-security 上公告,這個編號 CVE-2026-93990 的問題已於 2026 年 9 月 22 日隨 2.8.5 版修補。上游給出的 CVSS 3.1 評分為 9.8(AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H),屬於高風險等級;NVD 的 CVE 條目則列出另一組不同的 CVSS 向量。公告特別指出,這個問題與過去的 CVE-2022-25235 屬於同一類型。
漏洞機制
UTF-16 用一對代理項(surrogate pair)來編碼超出基本平面的字元:高代理項(U+D800 至 U+DBFF)必須緊接著一個低代理項(U+DC00 至 U+DFFF),兩者合起來才是合法的單一字元。libexpat 修補前的解碼邏輯沒有檢查高代理項後面是否真的接了低代理項,於是一段只有高代理項、沒有低代理項的孤立序列會被當成正常內容繼續往下傳遞。這正是公告所說的「走私」:驗證原本該由 Expat 完成,卻被跳過,讓不合法的 UTF-16 混進了應用程式看到的解析結果。當同一份輸入被不同元件用不同規則各自解讀(例如另一套嚴格驗證是否合法的程式邏輯),就可能出現兩邊對「這份 XML 到底寫了什麼」認知不一致的情況,而不一致正是後續傷害能否被觸發的關鍵。公告寫明實際造成的破壞程度,取決於使用 Expat 的應用程式如何處理這種畸形 UTF-16,理論上可從資料錯亂到更嚴重的邏輯後果都有可能。
這類問題可以類比網路層常見的走私手法:某一層做過合法性檢查、判定輸入乾淨,另一層卻用不同規則重新解讀同一段位元組,兩邊「看到的內容」因此不一樣,攻擊者只要抓準這個縫隙,就能讓原本該被拒絕的資料在下游悄悄復活。放在 XML 場景裡,前後端可能是同一支程式裡的不同模組,也可能是先經過一層安全過濾、再交給以 libexpat 剖析的下游服務。公告點名的 CVE-2022-25235,成因同樣是 Expat 在判斷位元組序列是否為合法編碼時出錯,讓原本該被拒絕的輸入通過了檢查;這次的孤立高代理項問題,本質上也是「該擋的沒擋住」,只是換了一個編碼層面的疏漏。
受影響版本
- 受影響:libexpat 2.8.5 之前的版本(官方公告與 2.8.5 changelog 皆未列出明確的起始版本下限)
- 修補版本:libexpat 2.8.5,2026 年 9 月 22 日發布
- 對照:性質類似的舊漏洞 CVE-2022-25235,同樣涉及 Expat 對編碼合法性的檢查疏漏
修補與緩解
libexpat 是被廣泛內嵌的 XML 解析函式庫,不少程式語言的標準函式庫(例如 Python 的 xml.parsers.expat)與大量 Linux 套件都直接連結它,因此波及面不只是「有沒有裝 libexpat」這麼單純。真正該檢查的,是自己的專案或相依鏈裡有沒有連結到舊版 libexpat,包括某些專案為了減少外部相依而自行 vendor 一份 Expat 原始碼、跟著系統套件更新走不到的情況。可執行的動作很直接:把系統或應用內的 libexpat 升級到 2.8.5,並對 vendored 副本額外確認版本號,不能只靠套件管理員回報的系統層 libexpat 版本。公告連結的修補 pull request(GitHub 上的 #1282)與 2.8.5 changelog 都可以用來核對自己拿到的版本是否已經包含這次修正。
優先順序上,任何直接解析不受信任XML 輸入的服務都該排在最前面,例如對外開放的 API、檔案上傳後會被解析的格式、或是內部系統之間傳遞設定檔的管線。這些場景裡,一旦畸形 UTF-16 被 Expat 放行,程式後續拿到的字串內容就可能和原始位元組所代表的意圖不同,等於是在信任邊界內埋了一個認知落差。升級前也值得盤點這些服務用的是系統套件裡的 libexpat,還是編譯時靜態連結、或內嵌在容器映像裡的版本,三種情況的更新方式並不相同。
原始來源:
- https://oss-security.openwall.org/lists/oss-security/2026/09/22/8
- https://github.com/libexpat/libexpat/blob/R_2_8_5/expat/Changes