強化 npm 相依套件的安全性
原文由 Alex O'Callaghan 于 發布,訂閱此部落格
2026 年 3 月 30 日,兩個遭植入惡意程式碼的 axios 版本曾短暫被發布到 npm 上。Axios 每週下載量超過一億次。攻擊者入侵了一位維護者的帳號,並利用該帳號發布了 [email protected] 與 [email protected],兩個版本都包含一個隱藏的相依套件,其 postinstall 鉤子會悄悄安裝跨平台的遠端存取木馬(Remote Access Trojan)。
惡意版本在被下架前大約存活了三小時,微軟將這起攻擊歸因於北韓政府支持的駭客組織。這是一起針對生態系中最廣泛使用的套件之一、經過縝密策劃的供應鏈攻擊行動。
這件事正好提醒我們,該好好檢視自己到底做了哪些防護措施。
從最基本的做起:使用 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: trueminimumReleaseAge 會讓 pnpm 拒絕安裝在指定分鐘數內才發布的套件版本。這次 axios 攻擊的惡意版本只存活了三小時,只要設定一天的延遲(1440),就足以完全避開。我們則是設定為七天,與 Renovate 的穩定性等待期一致。
啟用 trustPolicy: no-downgrade 後,如果某個套件過去的版本是透過可信任的 CI 流程發布並附有來源證明(provenance attestation),但新版本卻缺少這項證明,pnpm 就會阻擋安裝。這次 axios 攻擊就能透過這種方式被偵測出來,因為惡意版本在發布時並未包含正規版本所具備的可信任發布者綁定。
有一點要注意:當正規套件的維護者不再提供來源證明時,no-downgrade 偶爾會產生誤判。你可以使用 trustPolicyExclude 來排除你已手動驗證過的特定套件:
trustPolicyExclude:
- "[email protected]"另外也請明確加上 blockExoticSubdeps: true。這能防止任何遞移相依套件從 Git 儲存庫或直接透過 tarball 網址解析,強制它們只能來自 registry。
為內部套件加上 Scope
如果你會將內部套件發布到私有的 registry,請確保它們都位於組織的 scope 之下(例如 @myorg/package-name),而非使用無 scope 的名稱。這可以降低相依套件混淆(dependency confusion)攻擊的風險,也就是攻擊者在公開的 registry 上發布與你內部套件同名或名稱相近的套件。一旦你的 registry 設定不小心被還原,或新環境被錯誤設定為優先從公開 registry 拉取,沒有 scope 的內部套件名稱就會成為顯而易見的攻擊目標。
使用 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 團隊即時將其下架。穩定性等待期只有在有人發現問題時才有效,它並不能取代你在新增相依套件與套用更新時應有的警覺。
隨機一篇部落格
留言
登入後參與討論