系列: 與 AI 一起學寫程式

毀掉 300 個檔案的那次尋找取代

一次深度稽核翻出了什麼,以及修得有多快。

兩個發現,一次稽核 當一次掃蕩比對到了超出它該比對的範圍 300 個檔案毀損 process_event→ 亂碼 on_success()→ 亂碼 has_access→ 亂碼 沒有以單字邊界限定範圍 編譯全綠,剖析正常 修法:從 git 歷史還原 硬編碼的密鑰 secret = "●●●●●●●●●●●" 看起來像佔位值 變成了真實的值 修法:輪換 + 環境變數 修法:CI 裡的密鑰掃描
損害發生在程式碼的人類層,編譯器從來不看那裡。

代理驅動的程式碼庫跑得快——也會以人類審查者一眼就能抓到的方式失效。這裡有兩次失效,公開講,還有現在能把它們攔死的護欄。

你去找麻煩的那一天,通常就是找到麻煩的那一天。這個星期六我排了一次對程式碼庫的深度稽核。計畫是清理技術債:裸露的 except 區塊、硬編碼的常數、慣例的漂移,任何全速前進的專案都會堆積出來的那種東西。

我找到了兩件我不樂見的事。兩者現在都修好了。兩者都是我想公開講的那種發現,因為它們解釋了某些真實的東西:代理驅動的程式碼庫如何失效,以及做稽核的紀律如何抓住那些失效。

發現一:吃掉整個程式碼庫的尋找取代

專案早期,有一個代理被要求對 docstring 和註解做一次行銷風格的掃蕩。意圖是合理的:把一句出現在少數對外字串裡的特定用語改名。執行時對單字邊界不夠謹慎。

代理要替換的那句話,是一句行銷標語,其中包含了「process」、「success」和「access」這幾個詞,作為更長片語的一部分。尋找取代操作在不該比對到的地方比對到了這些子字串。變數名稱。函式名稱。測試描述。行內註解。凡是這三個子字串出現的地方,代理的替換字串就被塞了進去。

結果是三百個檔案裡帶著隱微亂碼的識別字與文字。名為 process_event 的變數變成了 token 中間嵌著「Raku Game Engine Milestone」的東西。函式描述讀起來像胡言亂語。測試描述聲稱在測試不存在的東西。程式碼庫能編譯,因為壞掉的識別字在各自的檔案內是一致的,但程式碼庫可供人閱讀的那一層,處處被以隱微的方式破壞了。

我想具體講清楚這類失效是怎麼發生的,因為這是一類其他團隊也會撞上的代理驅動失效。

尋找的範圍畫得太寬。代理收到的指令是找到一句話並替換它。這句話碰巧是常見英文單字的子字串。正確的限定方式是以單字邊界(正規表示式的 \bword\b)、明確的大小寫敏感度、明確的副檔名允許清單、明確的識別字語境拒絕清單來畫範圍。代理收到的指令一項限制都沒有。

代理沒有把規模標示出來。三百個檔案是很多檔案。一個為了小小的行銷調整而落地一個動到三百個檔案的 PR 的代理,本該在開 PR 的時候把規模標示出來。代理沒有。PR 標題寫著類似「更新 docstring 中的行銷文案」。PR 內文把檔案數列成一個數字,而不是一個疑慮。

我的審查流程沒有抓到它。PR 差異是三百個檔案的兩行小變更,每一個看起來都像同一種編輯。掃一眼,這份差異讀起來像一次乾淨的掃蕩。只有在你真正去讀某個檔案被改動的內容、而代理的替換恰好在那裡產出了胡話時,毀損才會現形。我沒有讀。我合併了。

CI 沒有抓到它,因為那些名稱仍然剖析得過。毀損的識別字在語法上是合法的。編譯器不在乎你的變數名稱看起來像一句行銷標語。建置是綠的。測試照跑。損害發生在程式碼的人類層,不在機器層。

這個星期六我怎麼修的

一支腳本。腳本做三件事。

其一:重新推導出正典的識別字名稱。腳本從壞掉的尋找取代落地之前的 git 歷史,重建每個識別字本來該叫什麼。重建是機械式的:對壞 PR 動到的每個檔案,把 PR 前的版本和 PR 後的版本做差異比對,對每一個被替換的 token,提議還原成 PR 前的名稱。大多數檔案能乾淨還原。少數需要人工審查,因為它們在毀損之上又疊了合法的變更。

其二:一輪 grep 驅動的健全性檢查。即使還原之後,某些毀損的識別字已被壞 PR 落地之後才寫的新程式碼參照。那些參照是照著毀損的名稱寫的。grep 這一輪找出壞 PR 落地後所寫程式碼中對毀損風格識別字的每一個參照,並逐一標記交由人工裁決:這段新程式碼是刻意要用毀損的名稱(罕見),還是只是用了當時碰巧存在的名稱(多數情況)?

其三:面向未來的護欄。代理現在做的每一次尋找取代操作,都必須指明 (a) 單字邊界限定、(b) 大小寫敏感度、(c) 副檔名允許清單、(d) 檔案數上限門檻,超過就必須標示並請求明確審查,以及 (e) 在套用完整替換之前,代理必須先展示三個隨機比對樣本。這道護欄寫進了 Copilot Guide,現在是每一個尋找取代任務框架的一部分。

毀損已經修復。做這次修復的稽核腳本就在版本庫裡,隨時可跑,差異輸出留存為證據。教訓寫進了 Copilot Guide。

發現二:硬編碼的 HMAC 密鑰

這次深度稽核還翻出了一件我早該抓到的事。執行時期的授權層使用 HMAC-SHA-256 驗證授權權杖。HMAC 密鑰被硬編碼在一個原始檔裡。那個原始檔在公開的版本庫裡。那個密鑰是真實正式驗證路徑所使用的真實密鑰。

這是今天最難堪的發現。我想對它誠實,因為這正是快速前進的代理驅動程式碼庫會發生的那種事,而公開討論怎麼抓到它,比私下討論更有價值。

它落地的路徑是這樣的:授權層的早期版本用一個佔位密鑰值做了原型,本意是在這一層出貨給任何人之前替換掉。原型帶著一個一看就是開發用佔位值的東西在 PR 裡落地。隨著時間推移,真實的驗證邏輯疊在了佔位值之上。佔位值一旦被包進看起來很真實的驗證程式碼裡,就不再像佔位值了。等到有人注意到時,這個密鑰已經被用在正式風格的流程裡,而那個檔案在公開版本庫裡。

我今天做了什麼:

  • 輪換了密鑰。外洩的值不再是正式環境的值。新值放在環境變數裡,並為開發環境提供一個 warnings.warn() 後備,讓開發工作可以在沒有真實密鑰的情況下進行,但會大聲嚷嚷這件事。
  • 從原始檔中移除了硬編碼的值。取而代之的是一個 getenv,在正式建置中若環境變數未設定,會給出清楚的錯誤訊息。
  • 加了一道 CI 檢查,掃描符合常見模式的硬編碼密鑰(高熵字串、base64 形狀的權杖、任何看起來像金鑰的東西)。這道檢查是那種能在下一次企圖落地之前就攔住它的小型基礎設施。
  • 開了一個後續事項,稽核程式碼庫其餘部分是否有類似模式。那次稽核是另一個週末的工作。今天要做的是把眼前這個發現關掉。

授權層仍然能運作。新路徑更安全。外洩的密鑰在發現後數小時內就完成了輪換。

這能推廣到什麼

幾個誠實的重點。

代理驅動的尋找取代需要明確的範圍限定規則。這是專案歷史上第三次被範圍過寬的掃蕩咬到。前兩次損害較小。這一次糟到值得設一道永久護欄。護欄現在已經就位。

原始檔裡的硬編碼密鑰是紀律的失敗,不是工具的失敗。沒有任何工具救得了一個放任真實密鑰落進公開檔案的團隊。「每個提交都要審查有無硬編碼憑證」的紀律才是真正的修法。CI 掃描有幫助。紀律才是關鍵。

稽核找得到審查漏掉的東西。定期對程式碼庫跑一輪稽核、專門尋找逐 PR 審查容易漏掉的失效模式,這種紀律值得花時間。今天的稽核抓到了兩件 PR 審查放行了的事。未來的稽核會抓到別的。節奏本身就是重點。

夥伴與開發者該從中帶走什麼

如果你正在為合作評估一個引擎,問問那個團隊他們怎麼處理「代理驅動的範圍過寬掃蕩」這種失效模式。正確的答案包含明確的範圍限定規則、大型變更的強制標示,以及稽核輪次。錯誤的答案是「我們沒遇過那個問題」。

如果你自己在跑代理驅動的工作流程,而且最近沒做過硬編碼密鑰稽核,做一次。有東西溜進來的機率不是零。現在找到它的成本很小。

如果你是讀到這裡的資安專業人士、而且有建議,我是真心有興趣。我正在努力防禦的失效類別是「代理做了一件人類審查者一眼就能抓到的事,但在代理驅動工作流程所鼓勵的大批量審查模式裡沒被抓到」。歡迎提出建議。

星期六下午。程式碼庫被好好地看了一遍。兩個發現,都修好了。下一次稽核已經排上日曆。

回去繼續蓋。

一個為了被稽核而打造的執行時期

RakuAI 是 LLM 開發商與智慧眼鏡製造商用來打造產品的空間執行時期——以稽核立紀律,以公開的教訓淬鍊。看看我們如何為夥伴級的信任做工程。

← 所有文章