Hardening npm dependency security

Alex O'Callaghan

強化 npm 依賴安全性

2026 年 3 月 30 日,有兩個惡意的 axios 版本被短暫發布至 npm。Axios 每週下載量超過 1 億次。攻擊者入侵了某位維護者的帳號,並利用該帳號發布了 [email protected][email protected],每個版本都包含一個隱藏的依賴,其 postinstall 鉤子會悄悄安裝跨平台的 Remote Access Trojan(遠端存取木馬)。

這些惡意版本在被下架前上線了約三小時,Microsoft 將此次攻擊歸因於北韓的國家級行為者。這是一場針對生態系中最廣泛使用的套件之一、經過精心策劃的針對性供應鏈攻擊行動。

這是一個很好的提醒,讓你重新檢視自己究竟做了哪些保護措施。

從最基本的做起:使用 lockfile

這點雖然不言自明,但還是值得一提。請提交你的 lockfile。不要執行會繞過它的安裝操作。lockfile 的意義在於,你安裝的是上次解析後的確切版本,而不是當下符合 semver 範圍的任意版本。這也意味著供應鏈事件會以 diff 的形式呈現,讓你能在 transitive dependency(遞移依賴) 意外變更時察覺。

縮小依賴範圍

限制在供應鏈攻擊中曝險的最簡單方法,就是減少依賴數量。你沒有安裝的每一個套件,都是不會被入侵的套件。

首先,稽核未使用的依賴。Knip 會掃描你的程式碼庫,找出列在 package.json 中卻已不再於任何地方被引入的套件。專案隨著時間會累積無用的依賴,而大多數團隊不會主動清理。定期執行 Knip 或將其納入 CI 流程,可確保移除未使用的依賴。

其次,參考 e18e,這是一項專注於清理、現代化並提升 JavaScript 套件效能的生態系計畫。其中的一項工作是將笨重、過時的依賴替換為更輕量的現代替代方案,例如像 is-odd 這類根本不該作為依賴存在的套件、如今已原生支援的 lodash 函式等等。依賴越輕量,需要擔心的套件就越少。

pnpm 設定

axios 的 RAT 是透過會在套件安裝時自動執行的 postinstall 鉤子傳遞的。這是絕大多數 npm 供應鏈攻擊背後的機制。

pnpm v10 預設會停用依賴中 postinstall 腳本的自動執行。與其為任何提出要求的套件執行建置腳本,你需要明確地將真正有需要的套件加入允許清單:

# pnpm-workspace.yaml
allowBuilds:
  esbuild: true
  "@parcel/watcher": true

如果某個依賴先前不需要建置腳本,它就不會突然執行。即使套件的某個版本遭到入侵,只要該 postinstall 鉤子從未被加入允許清單,就無法利用它來執行惡意程式碼。

你也應該啟用幾個 pnpm 設定來加強保護:

minimumReleaseAge: 10080 # 7 days in minutes
trustPolicy: no-downgrade
blockExoticSubdeps: true

minimumReleaseAge 會讓 pnpm 拒絕安裝發布未滿指定分鐘數的任何套件版本。axios 攻擊的存活時間為三小時。僅僅一天的延遲(1440)就足以完全避開它。我們採用七天,與我們的 Renovate 穩定性時間窗一致。

trustPolicy: no-downgrade 的設定下,若套件先前是透過受信任的 CI 流程發布並附有 provenance attestation(來源證明),但新版本缺乏該證明,pnpm 就會阻擋安裝。axios 攻擊正是透過這種方式被偵測到,因為惡意版本在發布時並未包含正規版本中原有的可信任發布者綁定。

有一點要注意:當正規套件的維護者移除 provenance attestation 時,no-downgrade 確實偶爾會產生誤判。你可以使用 trustPolicyExclude 來豁免你已手動驗證過的特定套件:

trustPolicyExclude:
  - "[email protected]"

也請明確加入 blockExoticSubdeps: true。這可防止任何 transitive dependency 從 Git 儲存庫或直接的 tarball 網址解析,強制它們必須來自 registry。

為內部套件設定範疇

如果你會將內部套件發布到私有的 registry,請確保它們位於某個組織範疇之下(例如 @myorg/package-name),而非使用無範疇的名稱。這能降低 dependency confusion(依賴混淆) 攻擊的風險,也就是攻擊者在公開 registry 上發布與你內部套件同名或名稱相似的套件。若你的 registry 設定曾經還原或新環境被錯誤設定為優先從公開 registry 拉取,無範疇的內部套件名稱就會成為輕易下手的目標。

使用 Renovate 自動升級

我們使用 Renovate 來管理各專案間的依賴更新。這裡有兩個設定會協同運作。

minimumReleaseAge(原為 stabilityDays)會延遲 Renovate 為新套件版本建立 PR 的時間,直到該版本發布滿指定的天數。我們將其設為七天。這讓社群有時間在惡意版本進入我們的程式碼庫之前將其攔截,附帶的好處是也能避免在某個版本發布兩天後又推出修補程式所帶來的頻繁異動。

安全性更新的 PR 會完全繞過 minimum release age 的限制。如果 Renovate 在某個依賴中偵測到已知漏洞,無論修補程式發布了多久,它都會立即建立 PR。穩定性延遲不會拖慢你對 CVE 的回應速度,它只會放緩例行性的版本升級。

對於內部套件,請設定獨立的 package rule,且不加上穩定性延遲。你會希望快速推送這些更新,以便及早發現整合問題。

{
  "minimumReleaseAge": "7 days",
  "packageRules": [
    {
      "matchPackagePrefixes": ["@myorg/"],
      "minimumReleaseAge": "0 days"
    }
  ]
}

這些都不是萬靈丹

這些設定的作用在於爭取時間,而非自行偵測任何問題。我們仰賴資安研究人員、自動化掃描平台與開源社群來識別遭到入侵的套件,並仰賴 npm registry 團隊即時將其移除。穩定性時間窗只有在他人發現問題時才會發揮作用,它無法取代你在新增依賴與引入升級時應有的警覺。

原文由 Alex O'Callaghan 發布

本文章由 muse-spark-1.2-contributor 進行翻譯