實(shí)戰(zhàn):命令注入與反序列化漏洞修復(fù)指南)
最近在給一個(gè)自己維護(hù)的開源工具做安全專項(xiàng)審計(jì)翻了兩個(gè)月代碼干掉了一批命令注入和反序列化相關(guān)的隱患。這個(gè)過程讓我意識(shí)到一件事很多開源項(xiàng)目不是不重視安全而是完全不知道漏洞會(huì)長(zhǎng)在哪些不起眼的位置。市面上講漏洞的文章不少但真正能落地的防御思路和修復(fù)實(shí)操卻不多見。這篇文章我會(huì)用實(shí)戰(zhàn)視角復(fù)盤一次完整的安全審計(jì)過程重點(diǎn)拆解兩類我在開源工具里反復(fù)遇到的高危問題命令注入和反序列化。內(nèi)容包括漏洞成因、典型錯(cuò)誤寫法、修復(fù)方案、繞過手法以及一套能在日常開發(fā)里直接落地的防御閉環(huán)。無論你是后端開發(fā)者、安全工程師還是開源項(xiàng)目維護(hù)者只要你的代碼里有“接收外部輸入再處理”的環(huán)節(jié)這篇文章都值得讀一遍。1. 安全審計(jì)這件事到底在做些什么1.1 開源工具為什么容易成為攻擊者的“重點(diǎn)關(guān)照對(duì)象”開源工具和其他軟件有一個(gè)本質(zhì)區(qū)別代碼完全公開。攻擊者不需要做黑盒探測(cè)不需要靠報(bào)錯(cuò)信息猜邏輯直接拉源碼看就完了。哪里的輸入沒有校驗(yàn)、哪里把用戶輸入拼進(jìn)了系統(tǒng)命令、哪里調(diào)了不安全的反序列化接口在代碼倉(cāng)庫(kù)里幾乎是裸奔的。再加上開源工具通常被大量項(xiàng)目集成使用場(chǎng)景五花八門。同一個(gè)工具有人把它跑在公網(wǎng)服務(wù)里有人把它放在內(nèi)網(wǎng)任務(wù)平臺(tái)上還有人直接以管理員權(quán)限部署在服務(wù)器上。一旦工具本身存在漏洞攻擊者拿到的就是一把通用的鑰匙能打開所有集成方的門。我做審計(jì)的這款工具是一個(gè)用來處理各類數(shù)據(jù)文件的解析組件功能不算復(fù)雜但輸入來源非常多命令行參數(shù)、配置文件、網(wǎng)絡(luò)上傳的文件、消息隊(duì)列里的消息體。每一個(gè)入口畫出來之后我自己都吸了一口涼氣代碼里至少有七八處地方在“默認(rèn)輸入可信”的前提下直接使用數(shù)據(jù)。開源項(xiàng)目出安全問題還有一個(gè)現(xiàn)實(shí)原因維護(hù)者精力有限。大部分開源項(xiàng)目的核心維護(hù)者是幾個(gè)人甚至是一個(gè)人日常要管功能迭代、issue回復(fù)、版本發(fā)布安全審計(jì)這種不產(chǎn)生新功能的活很容易被排在最后。等到安全掃描工具爆出高危漏洞往往已經(jīng)處于被利用的邊緣。1.2 審計(jì)之前先把攻擊面畫出來很多第一次做安全審計(jì)的人容易犯一個(gè)錯(cuò)誤拿著代碼從頭到尾逐行讀讀了兩天還在前幾個(gè)模塊打轉(zhuǎn)效率極低。正確的做法是先畫攻擊面把“外部數(shù)據(jù)能觸達(dá)的代碼路徑”全部找出來再順著這些路徑往深處審計(jì)。對(duì)于一個(gè)典型的開源工具攻擊面通常集中在幾個(gè)地方命令行參數(shù)解析用戶通過命令行傳參參數(shù)會(huì)被拼接到系統(tǒng)命令或文件路徑中配置文件讀取配置項(xiàng)如果包含路徑、URL、命令模板就可能成為注入點(diǎn)網(wǎng)絡(luò)接口監(jiān)聽端口的服務(wù)收到的請(qǐng)求體、請(qǐng)求頭、查詢參數(shù)都是不可信輸入外部文件處理文件名、文件內(nèi)容、元數(shù)據(jù)都可能是攻擊者構(gòu)造的消息隊(duì)列或插件系統(tǒng)消息字段和插件參數(shù)往往被直接傳入內(nèi)部邏輯。我建議把攻擊面整理成一張清單標(biāo)注每條路徑的輸入來源、經(jīng)過的代碼模塊、最終到達(dá)的危險(xiǎn)函數(shù)。這個(gè)步驟看起來費(fèi)時(shí)間但能幫你在后續(xù)審計(jì)中快速聚焦高風(fēng)險(xiǎn)區(qū)域而不是像無頭蒼蠅一樣亂撞。順手說一句畫攻擊面的時(shí)候要刻意區(qū)分“間接外部輸入”和“直接外部輸入”。很多漏洞出在間接路徑上比如一個(gè)函數(shù)接收的是內(nèi)部調(diào)用傳來的參數(shù)但參數(shù)值最終來自某個(gè)網(wǎng)絡(luò)請(qǐng)求這種數(shù)據(jù)流在代碼里繞了兩三層之后很容易被審計(jì)者忽略。2. 命令注入開源工具里最經(jīng)典的“翻車點(diǎn)”2.1 命令注入的成因信任了不該信任的輸入命令注入說穿了就一句話程序把外部輸入當(dāng)成操作系統(tǒng)命令的一部分去執(zhí)行了。它最常見的原因是研發(fā)為了方便直接把用戶輸入拼接進(jìn)命令字符串再交給系統(tǒng)Shell執(zhí)行。只要輸入里帶上了分號(hào)、豎線、重定向這類特殊字符命令就變味了。我審計(jì)的項(xiàng)目里有一個(gè)典型場(chǎng)景工具支持通過參數(shù)指定一個(gè)目標(biāo)地址然后用系統(tǒng)命令去做連通性檢查。最早的代碼寫得很直白String input request.getParameter(target); ProcessBuilder builder new ProcessBuilder(/bin/sh, -c, ping -c 3 input); Process process builder.start();這段代碼的問題是如果攻擊者把target傳成127.0.0.1; cat /etc/passwd最終執(zhí)行的命令就會(huì)變成ping -c 3 127.0.0.1; cat /etc/passwd分號(hào)后面那段命令會(huì)原封不動(dòng)地被Shell執(zhí)行。攻擊者不需要什么高級(jí)技巧只要能控制輸入里的一個(gè)片段就能在服務(wù)器上執(zhí)行任意命令。命令注入的危害等級(jí)在安全圈子里常年排在第一梯隊(duì)原因很簡(jiǎn)單它直接通向操作系統(tǒng)。一旦被利用攻擊者拿到的往往是和工具進(jìn)程相同的權(quán)限。如果工具以root或管理員身份運(yùn)行那這臺(tái)機(jī)器基本就拱手讓出去了。2.2 三種典型錯(cuò)誤與匹配的修復(fù)方案我在不同開源項(xiàng)目里見過大量命令注入寫法五花八門但歸納起來基本是三類。下面把這三類錯(cuò)誤寫法和對(duì)應(yīng)的修復(fù)方案放在一起說。錯(cuò)誤一字符串直接拼接進(jìn)Shell命令剛才那個(gè)Java例子就是典型。修復(fù)方式有兩個(gè)層次第一層能不經(jīng)過Shell就不要經(jīng)過Shell把命令和參數(shù)拆開傳。第二層對(duì)輸入做嚴(yán)格的格式校驗(yàn)。String input request.getParameter(target); if (!input.matches(^[a-zA-Z0-9.-]$)) { throw new IllegalArgumentException(非法目標(biāo)地址); } ProcessBuilder builder new ProcessBuilder(ping, -c, 3, input); Process process builder.start();這里的關(guān)鍵是ProcessBuilder的列表形式不會(huì)像/bin/sh -c那樣把參數(shù)交給Shell解釋特殊字符不再具有命令解析語(yǔ)義。配合正則白名單攻擊者根本沒有機(jī)會(huì)把分號(hào)、豎線這類字符送進(jìn)來。錯(cuò)誤二Python里圖省事直接開shellPython開發(fā)里面同樣的問題泛濫最常見的是subprocess模塊的shellTrue。import subprocess user_input get_user_input() subprocess.call(ping -c 3 user_input, shellTrue)shellTrue意味著系統(tǒng)會(huì)啟動(dòng)一個(gè)Shell來解析整條命令拼接進(jìn)來的用戶輸入會(huì)被當(dāng)作命令的一部分。修復(fù)同樣分兩步把shellTrue去掉改用參數(shù)列表同時(shí)做輸入校驗(yàn)。import subprocess user_input get_user_input() if not re.fullmatch(r[a-zA-Z0-9.-], user_input): raise ValueError(非法目標(biāo)地址) subprocess.call([ping, -c, 3, user_input], shellFalse)參數(shù)列表的形式會(huì)把每個(gè)元素當(dāng)作獨(dú)立參數(shù)傳給程序不會(huì)觸發(fā)Shell的解析邏輯安全性明顯提升。錯(cuò)誤三黑名單過濾繞過還有一類“半吊子修復(fù)”看起來做了防護(hù)實(shí)際上一戳就破。最常見的就是黑名單把;、|、、$這些字符過濾掉認(rèn)為這樣就安全了。這種做法的問題在于黑名單永遠(yuǎn)無法窮舉所有危險(xiǎn)輸入。Shell解析支持大量的轉(zhuǎn)義、編碼和變量展開寫法一個(gè)分號(hào)被過濾了還有換行符、反引號(hào)、$()、大括號(hào)擴(kuò)展等著你。我見過一個(gè)項(xiàng)目把;和|過濾得很干凈結(jié)果攻擊者用反引號(hào)執(zhí)行命令照樣拿下服務(wù)器。黑名單的替代方案永遠(yuǎn)是白名單。對(duì)命令參數(shù)做“只允許哪些字符出現(xiàn)”的校驗(yàn)而不是“不允許哪些字符出現(xiàn)”。白名單可能誤傷合法輸入但可以通過和業(yè)務(wù)方確認(rèn)來補(bǔ)充字符集安全性和可用性是可以兼顧的。2.3 命令注入修復(fù)中容易忽略的三件事第一件修復(fù)命令注入不只是把API換掉還要檢查整條數(shù)據(jù)鏈路。你可能在A函數(shù)里做了參數(shù)化處理但B函數(shù)在調(diào)用A之前已經(jīng)用字符串模板拼過一遍。這種情況在代碼審計(jì)里很常見修了一處漏了另一處攻擊者照樣能打進(jìn)來。修復(fù)時(shí)要順著數(shù)據(jù)流把上游和下游全部看一遍。第二件參數(shù)列表形式不等于絕對(duì)安全。有些程序本身會(huì)把參數(shù)當(dāng)作命令解析比如某些工具支持接收-e這類參數(shù)來執(zhí)行代碼你不經(jīng)過Shell程序內(nèi)部也會(huì)自己解釋。所以參數(shù)化執(zhí)行只是減少了一部分風(fēng)險(xiǎn)輸入校驗(yàn)這條線不能省。最好的做法是白名單校驗(yàn)加參數(shù)化執(zhí)行雙管齊下。第三件命令執(zhí)行失敗時(shí)的報(bào)錯(cuò)信息也可能泄密。有些工具會(huì)把完整的命令字符串或輸出內(nèi)容原樣寫到日志里攻擊者看到日志后能反向推斷系統(tǒng)環(huán)境和內(nèi)部路徑。建議日志里只記錄脫敏后的參數(shù)名和錯(cuò)誤碼不要保留完整的命令參數(shù)。3. 反序列化一次readObject引發(fā)的“連鎖爆炸”3.1 反序列化漏洞的原理與利用鏈反序列化是另一種嚴(yán)重程度同樣拉滿的漏洞類型。它和命令注入的思路完全不同命令注入是騙程序執(zhí)行命令反序列化是騙程序加載攻擊者構(gòu)造的對(duì)象而在對(duì)象構(gòu)造過程中代碼里的某些危險(xiǎn)方法會(huì)被自動(dòng)觸發(fā)。先講一下原理。為了在網(wǎng)絡(luò)傳輸或持久化存儲(chǔ)時(shí)保留對(duì)象結(jié)構(gòu)很多語(yǔ)言提供了“序列化”機(jī)制把內(nèi)存對(duì)象轉(zhuǎn)成字節(jié)流之后再用“反序列化”把字節(jié)流還原成對(duì)象。問題出在“還原”這個(gè)動(dòng)作它不只是簡(jiǎn)單分配內(nèi)存、填充字段而是會(huì)執(zhí)行對(duì)象內(nèi)部的一些方法。以Java為例ObjectInputStream.readObject()在讀取過程中會(huì)調(diào)用對(duì)象的readObject()方法遍歷字段時(shí)還可能觸發(fā)各種類的readObject、readResolve、toString、hashCode等方法。如果類路徑上存在某個(gè)依賴庫(kù)它里面從readObject到危險(xiǎn)方法之間有一條可用的調(diào)用鏈攻擊者就可以精心構(gòu)造字節(jié)流讓反序列化過程一路走到命令執(zhí)行。這類利用鏈有個(gè)專門說法gadget chain。鏈條越長(zhǎng)觸發(fā)條件越苛刻也越容易被補(bǔ)丁打斷。但現(xiàn)實(shí)中很多組件的gadget chain構(gòu)造得相當(dāng)精巧攻擊者拿到一個(gè)精心構(gòu)造的惡意字節(jié)流發(fā)給目標(biāo)程序一次readObject就能完成遠(yuǎn)程命令執(zhí)行整個(gè)過程不需要任何身份認(rèn)證。Python的pickle模塊風(fēng)險(xiǎn)更直接因?yàn)樗葱蛄谢瘯r(shí)可以指定任意可調(diào)用對(duì)象幾乎等于把代碼執(zhí)行能力直接交給輸入方。import pickle class User: def __init__(self, name): self.name name def load_user(data): obj pickle.loads(data) return obj.name如果data來自不可信來源攻擊者可以構(gòu)造特殊的pickle字節(jié)流在反序列化時(shí)執(zhí)行任意函數(shù)。很多Python項(xiàng)目把pickle用在Redis緩存、消息隊(duì)列、臨時(shí)文件等場(chǎng)景一旦數(shù)據(jù)被污染就是一處隱蔽的高危漏洞。3.2 為什么“不要反序列化不可信數(shù)據(jù)”仍然防不住安全圈里有一句老話永遠(yuǎn)不要反序列化不可信數(shù)據(jù)。聽起來無懈可擊但在真實(shí)項(xiàng)目里根本做不到。原因之一很多歷史代碼在編寫時(shí)接口設(shè)計(jì)上并沒有區(qū)分“可信數(shù)據(jù)”和“不可信數(shù)據(jù)”。一個(gè)內(nèi)部服務(wù)本來只接收自己的緩存數(shù)據(jù)后來對(duì)外開放了一個(gè)接口傳進(jìn)來的內(nèi)容直接被反序列化這個(gè)風(fēng)險(xiǎn)就悄悄出現(xiàn)了。原因之二反序列化通常不是顯眼的第一步。有些代碼會(huì)先對(duì)數(shù)據(jù)做一層Base64解碼再解壓最后才丟給readObject。審計(jì)者如果只盯著readObject這一個(gè)調(diào)用點(diǎn)而忽略了整個(gè)數(shù)據(jù)處理流程很容易漏掉真正的入口。原因之三哪怕你在入口處加了“數(shù)據(jù)完整性校驗(yàn)”比如簽名或者哈希攻擊者也可能通過其他路徑繞過校驗(yàn)直接調(diào)用內(nèi)部的反序列化邏輯。我見過一個(gè)系統(tǒng)入口做了很完善的簽名驗(yàn)證但內(nèi)部某個(gè)調(diào)試接口沒有加校驗(yàn)照樣存在反序列化風(fēng)險(xiǎn)。所以防御不能只靠一條原則需要在代碼層面做縱深防御入口限制、過濾規(guī)則、運(yùn)行時(shí)審計(jì)每一層都不能少。3.3 實(shí)戰(zhàn)加固過濾、白名單、替換協(xié)議Java 9之后的版本提供了一個(gè)非常實(shí)用的機(jī)制ObjectInputFilter可以在反序列化時(shí)對(duì)類進(jìn)行過濾。用法很簡(jiǎn)單ObjectInputFilter filter ObjectInputFilter.Config.createFilter( com.example.dto.**;java.lang.*;!* ); ObjectInputStream ois new ObjectInputStream(inputStream); ois.setObjectInputFilter(filter); Object obj ois.readObject();這段過濾器表達(dá)式的意思是只允許反序列化com.example.dto包及其子包下的類以及java.lang包下的基礎(chǔ)類其他任何類一律拒絕。白名單策略比黑名單可靠得多因?yàn)槟悴槐孛杜e所有攻擊者的利用鏈只需要列清楚業(yè)務(wù)真正需要的對(duì)象類型。注意在設(shè)置過濾器時(shí)一定要把規(guī)則放到readObject()之前并且優(yōu)先用白名單而不是黑名單。我見過有人把過濾器寫成“只禁止某個(gè)惡意類”結(jié)果攻擊者換一個(gè)類就繞過了這種沒有任何防御價(jià)值。如果項(xiàng)目還在早期階段我強(qiáng)烈建議評(píng)估一下是否可以換掉原生反序列化協(xié)議改用JSON或更安全的序列化格式。對(duì)Java項(xiàng)目可以考慮使用只序列化字段、不執(zhí)行自定義方法的方案對(duì)Python項(xiàng)目能用json就盡量不要用pickle。有些場(chǎng)景確實(shí)無法替換比如跨語(yǔ)言調(diào)用時(shí)依賴了歷史協(xié)議。這種情況下另一個(gè)加固手段是把反序列化放到單獨(dú)的沙箱進(jìn)程里降低權(quán)限、限制網(wǎng)絡(luò)、監(jiān)控日志就算被利用攻擊面也會(huì)被大幅縮小。4. 構(gòu)建實(shí)戰(zhàn)防御體系上線前的安全閉環(huán)4.1 把安全設(shè)計(jì)前移到需求階段等代碼寫完再做安全評(píng)審效果一定打折。真正靠譜的做法是在需求評(píng)審階段就引入安全問題這個(gè)功能接收什么輸入輸入來自哪個(gè)信任邊界數(shù)據(jù)要經(jīng)過哪些處理如果數(shù)據(jù)是攻擊者可控的最壞情況會(huì)發(fā)生什么我以前參與過一個(gè)開源項(xiàng)目開發(fā)階段只考慮了功能一個(gè)“批量導(dǎo)入配置”的需求最后做成了直接反序列化上傳文件。如果安全人員在需求階段就站出來問一句“這個(gè)文件格式是什么、反序列化之后做什么”大概率能在設(shè)計(jì)層面規(guī)避這個(gè)問題。安全設(shè)計(jì)前移并不復(fù)雜關(guān)鍵是培養(yǎng)一種思維慣性任何外部輸入進(jìn)入代碼之前都要過一次“信任分級(jí)”。來自用戶請(qǐng)求的參數(shù)是不可信的來自內(nèi)部加密簽名的數(shù)據(jù)是半可信的來自本進(jìn)程內(nèi)部?jī)?nèi)存的對(duì)象才算完全可信。分級(jí)之后你在每個(gè)邊界上都知道該加什么校驗(yàn)。4.2 自動(dòng)化審計(jì)的節(jié)奏與人工確認(rèn)現(xiàn)在業(yè)界有很多代碼靜態(tài)分析工具能自動(dòng)掃描命令注入、反序列化這類已知風(fēng)險(xiǎn)模式。它們的價(jià)值在于覆蓋面廣、速度快能在一堆代碼里快速篩出可疑調(diào)用點(diǎn)。但自動(dòng)化工具也有明顯的短板誤報(bào)率高且對(duì)業(yè)務(wù)上下文不敏感。我碰到過很多次工具報(bào)了“命令注入”人工看代碼后發(fā)現(xiàn)輸入早就被白名單校驗(yàn)過了拼接和參數(shù)化也做了壓根沒有實(shí)際風(fēng)險(xiǎn)。反過來也有過一次工具沒報(bào)任何問題我人工審計(jì)時(shí)發(fā)現(xiàn)一個(gè)工具函數(shù)里用String.format拼了命令再把結(jié)果傳給了ProcessBuilder的shell模式妥妥的命令注入。所以我的建議是把自動(dòng)化掃描當(dāng)成“初篩機(jī)”把所有告警按風(fēng)險(xiǎn)級(jí)別排隊(duì)再安排人工逐條確認(rèn)。人工確認(rèn)的重點(diǎn)不是看工具報(bào)的那一行而是看從輸入到輸出的完整數(shù)據(jù)流。確認(rèn)無風(fēng)險(xiǎn)的要寫清楚理由確認(rèn)有風(fēng)險(xiǎn)的要立刻出修復(fù)方案。掃描結(jié)果不能直接交給開發(fā)了事否則漏報(bào)和誤報(bào)會(huì)把團(tuán)隊(duì)的安全信任感消耗殆盡。4.3 供應(yīng)鏈依賴審計(jì)與漏洞修復(fù)閉環(huán)開源工具的另一大風(fēng)險(xiǎn)來源是第三方依賴很多代碼審計(jì)只看了自己的代碼忽略了引用的組件里埋的雷。一個(gè)系統(tǒng)可能自己的代碼干干凈凈但某個(gè)依賴庫(kù)存在一個(gè)老漏洞被攻擊者利用后照樣能拿下整個(gè)服務(wù)。依賴審計(jì)要做在每一次版本變更時(shí)。新引入一個(gè)依賴之前先看它的版本號(hào)、活躍度、已知漏洞記錄每次依賴升級(jí)之后重新跑一遍全量掃描確認(rèn)沒有引入新的風(fēng)險(xiǎn)。很多包管理機(jī)制支持“自動(dòng)安全更新”但自動(dòng)更新也帶來兼容性風(fēng)險(xiǎn)最好先在一套獨(dú)立的測(cè)試環(huán)境里跑完整測(cè)試用例再合入主干。依賴漏洞修復(fù)閉環(huán)的關(guān)鍵是“快”但也要“穩(wěn)”。發(fā)現(xiàn)漏洞后先評(píng)估當(dāng)前版本是否存在可利用路徑如果有立刻升級(jí)到修復(fù)版本如果沒有記錄風(fēng)險(xiǎn)并持續(xù)跟蹤。升級(jí)之后要復(fù)測(cè)功能特別是涉及序列化、命令執(zhí)行、文件處理這些高風(fēng)險(xiǎn)模塊。5. 常見問題與排查技巧實(shí)錄5.1 審計(jì)與修復(fù)中的高頻問題速查表下面這張表整理了我在實(shí)際審計(jì)中遇過的一些典型問題以及對(duì)應(yīng)的排查思路。場(chǎng)景現(xiàn)象根本原因建議解法安全掃描檢出命令拼接代碼里有字符串拼進(jìn)系統(tǒng)命令拼接點(diǎn)存在于某些工具函數(shù)全量搜索命令執(zhí)行API改成參數(shù)列表加白名單校驗(yàn)反序列化過濾后業(yè)務(wù)異常加了白名單過濾器部分內(nèi)部對(duì)象不能加載過濾規(guī)則過嚴(yán)漏掉了內(nèi)部需要使用的類用運(yùn)行日志收集實(shí)際出現(xiàn)的類名生成動(dòng)態(tài)白名單并定期更新依賴升級(jí)后功能行為變化某個(gè)組件升完級(jí)接口返回值或者異常類型變了依賴大版本升級(jí)帶來的不兼容變更優(yōu)先小版本升級(jí)升級(jí)前跑全量回歸觀察變更日志用戶合法輸入被校驗(yàn)邏輯攔截輸入里包含、;等符號(hào)被當(dāng)成命令注入攔截全局校驗(yàn)規(guī)則過于粗暴按字段語(yǔ)義分開校驗(yàn)命令參數(shù)走格式白名單普通數(shù)據(jù)不要套用命令過濾反序列化接口被掃描發(fā)現(xiàn)接口接收字節(jié)流并直接readObject歷史接口設(shè)計(jì)時(shí)未考慮不可信來源增加ObjectInputFilter或替換為JSON等安全格式還有一個(gè)高頻問題值得單獨(dú)說很多團(tuán)隊(duì)以為“反序列化的類做一次白名單就夠了”但實(shí)際上同一個(gè)接口可能被多個(gè)模塊共用每個(gè)模塊需要的類集合不同。如果你只維護(hù)一份白名單要么過于寬松要么頻繁誤傷。比較好的做法是給每個(gè)反序列化入口配獨(dú)立的過濾配置配置里只放這個(gè)入口確實(shí)需要的類。5.2 來自實(shí)戰(zhàn)的三個(gè)提醒第一個(gè)提醒修復(fù)命令注入之前先做一次全量調(diào)用方盤點(diǎn)。我曾經(jīng)修完一個(gè)工具函數(shù)后才發(fā)現(xiàn)另一個(gè)模塊也在用這個(gè)函數(shù)而且傳入的參數(shù)格式完全不同。辛辛苦苦加的白名單正則直接把對(duì)方的合法輸入全攔截了線上灰度還沒開始就回滾了。正確流程是先找到所有調(diào)用點(diǎn)挨個(gè)確認(rèn)參數(shù)范圍再統(tǒng)一設(shè)計(jì)校驗(yàn)規(guī)則。第二個(gè)提醒反序列化加固不能只靠“增加過濾器”這一個(gè)動(dòng)作。過濾規(guī)則有版本兼容性老數(shù)據(jù)可能包含已經(jīng)被禁止的類升級(jí)后讀取歷史數(shù)據(jù)可能會(huì)失敗。你需要提前準(zhǔn)備數(shù)據(jù)遷移策略。另外一個(gè)容易漏的細(xì)節(jié)是自定義ClassLoader如果項(xiàng)目里用到了動(dòng)態(tài)類加載過濾器可能根本感知不到這些動(dòng)態(tài)類的存在。第三個(gè)提醒安全修復(fù)上線后一定要寫回歸測(cè)試用例。攻擊者構(gòu)造的payload就是最好的測(cè)試用例把它固化到測(cè)試代碼里每次改動(dòng)都跑一遍。我自己的習(xí)慣是給每個(gè)高危漏洞建立一個(gè)獨(dú)立的測(cè)試文件寫明“這個(gè)輸入以前會(huì)讓服務(wù)器執(zhí)行命令現(xiàn)在必須被攔截”。這種做法不僅防止自己后續(xù)改壞也為團(tuán)隊(duì)其他成員提供了一個(gè)直觀的安全風(fēng)險(xiǎn)案例。這段時(shí)間做審計(jì)最大的感觸是大多數(shù)漏洞都不是開發(fā)者故意留下的而是“方便”和“默認(rèn)信任”的產(chǎn)物。開發(fā)時(shí)順手拼個(gè)字符串覺得“不會(huì)有問題”接口里順手反序列化覺得“內(nèi)部數(shù)據(jù)很安全”??汕∏∈沁@些不起眼的順手之處成了攻擊者最喜歡的突破口。經(jīng)過這輪實(shí)戰(zhàn)我已經(jīng)把命令注入和反序列化的檢查項(xiàng)固化到了項(xiàng)目的安全清單里。凡是新增接口先走一遍攻擊面分析凡是涉及系統(tǒng)命令一律參數(shù)化加白名單凡是涉及對(duì)象反序列化一律過濾加白名單。后續(xù)開發(fā)新功能時(shí)我也會(huì)習(xí)慣性地問一句“如果這份輸入來自一個(gè)充滿惡意的陌生人這段代碼還安全嗎”這個(gè)習(xí)慣建議每個(gè)寫代碼的人都養(yǎng)成。