9  你在信什麼?把「信任一個套件」拆開

第二部把「沒被改過」這件事做到了盡頭:簽 digest、逐環境重驗、私鑰鎖進金庫。 但第 8 章結尾那句話還掛在那裡——簽章保證「沒被改」,擋不住「一開始就選錯」

這一部處理進來的東西。而第一件要做的事,是把一句每天說、卻幾乎沒人拆開看過的話 拆開:「這個套件可以信」——你到底在信什麼?

9.1 先預測:一行 npm install,你把信任交給了幾個人?

隨手挑一個現代前端專案,package.json 裡的直接依賴(direct dependency)通常是 幾十個。先預測:安裝完之後,node_modules 底下實際躺著多少個套件?

實務上的量級是幾百到上千——直接依賴的 5 到 10 倍是常見比例。差額全部來自 傳遞依賴(transitive dependency):你選的套件有它自己的依賴,那些依賴又有 自己的依賴。

flowchart TD
  Y["你宣告的<br/>直接依賴(數十)"] --> A["它們的依賴"]
  A --> B["它們的依賴的依賴"]
  B --> C["…(傳遞閉包,數百至上千)"]
  Y -.->|"你審過的"| Y
  C -.->|"你沒審過、但一樣會被載入執行"| C
图 9.1: 依賴冰山:你審過的是水面上那幾十個,實際載入的是水面下那幾百個。

這張圖給出一個不舒服但精確的結論:

你的信任決定作用在幾十個套件上,你的執行期風險作用在幾百個套件上。

這個落差不是誰疏忽造成的,是依賴管理的預設行為——npm installmvn compile 會安靜地把整個傳遞閉包(transitive closure)拉進來,不會問你任何一句。 Log4Shell 當年多數受害者說不出「我有用 Log4j」,原因就在這裡:他們確實沒有直接用。

9.2 把「信任一個套件」拆成四件事

第 5 章那張能力邊界表裡,④ Feed 那格的右欄寫著一句沒展開的話: 「身分正確 ≠ 身分可信」。現在展開它。當你說「這個套件可以信」,實際上你同時 默認了四個獨立的宣稱:

你默認的宣稱 誰能保證 密碼學管不管得到
① 我拿到的 bytes,就是那個專案發佈的那份 registry 的完整性機制、套件簽章、lockfile 鎖 hash 管得到——這是第一、二部整套工具的守備範圍
② 發佈它的人,還是我當初信的那個人 沒有東西保證 ❌ 維護者換手、帳號被接管,簽章照樣合法
③ 這段程式碼沒有惡意 沒有東西保證 ❌ 簽章驗鑰匙,不驗動機
④ 它下一版也安全 沒有東西保證 ❌ 信任是對過去的判斷,依賴是對未來的承諾

只有第 ① 條是密碼學問題。②③④ 全部是治理問題——而供應鏈攻擊,幾乎都打在 ②③④ 上。這解釋了一件初學時很困惑的事:為什麼那些出事的惡意套件, 簽章全都是合法的?因為它們根本沒偽造身分,它們就是那個身分本人發的。

注记第 ④ 條值得單獨想三十秒

①②③ 都是「此刻」的判斷,只有 ④ 是關於未來。而依賴這件事的本質是: 你今天核可一個套件,明天、後天、半年後它的新版本會持續流進你的產線。 一次性的審核,管的是一個持續發生的事。 這個時間落差,是第 11 章 「撤銷迴路」整章的由來。

9.3 四類風險:只盯 CVE,就只管到四分之一

依賴治理最常見的起手式是「裝一個掃描器掃 CVE」。這是必要的,但攤開來看, CVE 只覆蓋四類風險裡的一類:

風險類型 是什麼 CVE 掃描抓得到嗎
已知漏洞 元件有被登錄的安全缺陷(CVE) ✅ 這正是它的守備範圍
授權合規 授權條款與你的使用方式衝突——例如 copyleft 授權的元件進了不打算開源的商用系統 ❌ 完全不同的資料維度,要另外掃
惡意套件 上架的那一刻就帶著壞意圖(typosquat、被植入後門的版本) 結構性抓不到,下一節說明
維運風險 已停止維護、單一維護者、爆漏洞不會有人修 ❌ 這是專案健康度,不是漏洞資料

第三類值得特別咬一口,因為它是最反直覺的:

CVE 資料庫是回顧性的。一筆 CVE 存在,代表「有人發現了、分析了、登錄了」。 惡意套件上架的那一天,沒有任何資料庫裡有它——它是全新的、沒被研究過的東西。 拿漏洞資料庫去防惡意套件,等於拿一本通緝犯名冊去攔一個還沒犯案的人

那要拿什麼防?既然缺的是「還沒被發現」,補的就只能是時間——讓社群先看幾天。 這就推出下一章第一個原則的雛形:cooldown(冷卻期)。

9.3.1 cooldown 的兩個前提

先預測:要落地 cooldown,看起來只需要兩樣東西——一個數字(等幾天),和一個日期 (這個版本什麼時候出現的)。那個日期,你打算從哪裡讀?

這個問題比它看起來重要。同一句「版本發布時間」,至少有兩種來源:

日期來自哪裡 誰寫的 攻擊者改得動嗎
註冊中心的上架紀錄——npm registry metadata 的 time、Maven Central 的 upload 時間 註冊中心的伺服器 ❌ 改不動
套件自己帶的日期——manifest 裡的欄位、git tag 日期、build timestamp 發布者 ✅ 想填什麼填什麼

讀錯來源,cooldown 就從一道閘門變成一個裝飾。而它不會報錯:兩邊都是合法的日期字串, 程式跑起來一模一樣,你要等到出事才會知道自己量錯了東西。

重要這是本書第二次遇到同一個結構

第 3 章講憑證鏈時說過:簽章驗證成功只代表「鏈是完整的」,不代表「鏈的根是對的」。 這裡是同一個病換了個位置——你以為在量「世界第一次看到這個版本是什麼時候」, 實際在讀「發布者自己填的一個欄位」。

工具沒有壞,是你要的語意和你拿到的表示不是同一件事。往後每遇到一個 「拿 X 當作 Y 的證據」,都值得停下來問一句:X 這個欄位,是誰寫的?

第二個前提更難處理,因為它不是實作問題:cooldown 賭的是「這段時間會有人看」。 它自己不做任何檢查,它只是把你排到社群後面,指望在你踩到之前有人先踩到。

這個賭注贏過。xz-utils 的惡意版本 5.6.0 在 2024-02-24 上架、5.6.1 在 03-09, Andres Freund 在 03-29 揭露——一個 30 天的冷卻期會把 5.6.1 擋在門外,5.6.0 也只差幾天。

但要誠實看他是怎麼發現的:他在測 PostgreSQL 效能,注意到 sshd 登入多花了大約半秒, 覺得不對勁才追下去。那是運氣,不是控制。沒有任何機制保證下一次會有另一個 Andres Freund 剛好在跑 benchmark。

所以本章稍後那張威脅模型表把 cooldown 對 L3 標成「部分」,精確的意思是: 機制有效,但有效性不由你決定。你買到的是機率,不是保證——而知道自己買的是機率, 正是這一章想給你的東西。

9.4 攻擊者怎麼把套件送進你的產線

把上面的拆解翻到攻擊者那一側,路徑會很清楚。我在自己的實驗專案裡做過一份 攻擊樹(attack tree),把「讓惡意程式碼進 production」拆成分支;其中走依賴這條 路的,收斂成四種:

攻擊路徑 手法 打在哪個宣稱
打錯字搶註冊(typosquatting) 註冊一個與熱門套件差一個字母的名字,等人手滑 ③ 惡意——名字像,身分是攻擊者本人
維護者接管(maintainer takeover) 帳號被盜,或維護者把專案交棒給熱心陌生人 ② 換人了——簽章照樣合法,這正是 xz 那條路
命名空間混淆(dependency confusion) 在公開 registry 上註冊一個與你內部套件同名的套件,讓解析器優先抓到外面那個 ① 你以為拿到內部那份,實際上不是
換版本 依賴宣告不動,只把版本改成剛上架的惡意版 ③④ 舊版乾淨 ≠ 新版乾淨

看第二欄會發現一個共同點:沒有一條需要破解密碼學。它們全部繞過去—— 用一個合法的身分、發一份合法簽章的、有毒的內容。

9.5 誠實邊界:你防得到誰

拆到這裡,很容易滑進「所以什麼都防不了」的虛無。所以要把話講精確: 不同等級的攻擊者,成本與防法差很多。我自己的威脅模型分四級:

等級 攻擊者長什麼樣 防得到嗎
L1 機會主義 廣撒網的 typosquat、自動化掃弱點 ✅ 基礎衛生就擋得住:漏洞掃描+核可清單
L2 有針對性、技術中等 針對你的競爭者、心懷不滿的離職者 ✅ 加上政策審核、cooldown、來源強制
L3 長期經營(APT 級) xz 那種花兩三年養信任的臥底 🟡 部分——cooldown 給社群時間(xz 的時間軸差幾天就攔到),但擋不掉 long game,而且成敗不由你決定(見〈cooldown 的兩個前提〉)
L4 零時差+內部串通 多重內鬼配 0-day ❌ 防護成本遠超預期損失,明說接受這個殘餘風險

把 L4 寫進文件、而不是假裝防得到,是我認為一份治理設計是否誠實的分水嶺。 一套宣稱「全面防護供應鏈攻擊」的方案,通常只是沒把邊界寫出來。 寫下你不防什麼,比列出你防了什麼更有資訊量。

警告一個容易買錯的東西

市面上的工具幾乎都在賣第 ① 條和「已知漏洞」那一格——那是最容易做成產品的部分 (有資料庫、有明確答案、能出報表)。②③④ 沒有產品可買,因為它們的答案是 「你的組織決定要信誰」。所以評估工具時,別問「它能掃出多少漏洞」, 先問「我這四格裡,哪一格是我現在真正的缺口」


本章帶走的東西:依賴的傳遞閉包讓你審過的和你執行的差 5–10 倍;「信任一個套件」 拆開是四個獨立宣稱,密碼學只保得住第一個,其餘三個是治理問題;四類風險裡 CVE 掃描 只覆蓋一類,而惡意套件是結構性抓不到的(漏洞資料庫是回顧性的);四條攻擊路徑 沒有一條需要破解密碼學;cooldown 有兩個前提——日期要讀註冊中心而不是發布者填的欄位, 而且它賭的是「這段時間有人會看」;防護能力誠實地停在 L3,L4 是寫下來接受的殘餘風險。

往下一章的橋:既然 ②③④ 是治理問題,那治理要長成什麼樣才擋得住? 下一章把我在三個不同平台上重做過同一套依賴治理之後、收斂出來的五個原則攤開—— default-deny、單一判官、可見性民主化、cooldown、來源強制。原則之所以是原則, 是因為它們在三個平台上都活了下來;工具換了,這五條沒換。