鞏固你的 npm 供應鏈:開發者應對惡意套件的多層防禦指南
在現代軟體開發中,npm (Node Package Manager) 生態系統因其龐大的可重用程式碼模組庫而成為不可或缺的工具。 然而,這種開放與共 享的特性也使其成為日益增長的供應鏈攻擊的首要目標。
2024年,npm 平台總共披露了40,009個漏洞,較2023年增加了38%,其中有231個漏洞被評定為CVSS 10.0的嚴重等級 。 這些攻擊不再僅限於技術漏洞,更演變為利用開發者社群內的信任關係與既有流程。 本報告旨在為開發者提供一個全面、多層次且可操作的防禦框架,從根本上鞏固其開發環境。
本報告的核心觀點在於:主動式安全防禦已不再是可選項。
一個強大的防禦體系應從最早的套件審查階段開始,貫穿整個安裝過程,並在整個軟體生命週期中進行持續監控。
透過結合技術工具與安全至上的思維模式,開發者可以建立一個深度防禦(defense-in-depth)模型,顯著降低被惡意套件侵害的風險。
本報告將詳細闡述從預防、偵測到應對的每個安全層面,為開發者提供一套完整的實踐指南。
1. 現代威脅概況:理解 npm 供應鏈攻擊
現代軟體供應鏈攻擊複雜且多樣化,不再僅僅是利用已知的程式碼漏洞。
這些攻擊者利用人性弱點、平台機制和龐大的依賴關係樹,潛入開發環境並造成廣泛的危害。
1.1. 攻擊向量
攻擊者滲透 npm 生態系統的主要途徑多管齊下,其中最狡猾的幾種方式值得深入探討。
第一種是「維護者帳號劫持」。
攻擊者並非直接攻擊 npm 平台的程式碼,而是透過網路釣魚等社會工程手段,劫持了受信任套件維護者的帳號 。
一個著名的案例是 is 套件的妥協,攻擊者首先劫持了一位舊維護者的 npm 帳號。 隨後,他們並沒有立即發佈惡意版本,而是利用精心設計的騙局,聯絡當前維護者團隊,謊稱其帳號因未啟用雙重驗證(2FA)而被 npm 移除 。 這巧妙地利用了開發者社群中維護者之間固有的信任,成功說服當前團隊將被劫持的舊帳號重新加回維護者名單。 隔天,攻擊者便利用恢復的權限發佈了惡意版本3.3.1和5.0.0。這個案例清晰地揭示了,單純的技術防禦不足以應對利用信任關係和人類行為的攻擊。
第二種常見的攻擊手段是利用惡意生命週期腳本。 npm 允許套件維護者在 package.json 檔案中定義所謂的「生命週期掛鉤」(lifecycle hooks),例如 preinstall、install 和 postinstall 。 這些腳本會在套件安裝過程中的特定階段自動執行,通常用於編譯原生模組或執行配置任務。 然而,這些腳本也為攻擊者提供了完美的武器,使其可以在使用者安裝套件的瞬間,在目標機器上執行任意程式碼 。 例如,eslint-scope 套件的維護者帳號被入侵後,攻擊者發佈了包含惡意 postinstall 腳本的新版本,該腳本專門用於竊取使用者的 npm 憑證 。 另一個案例是 crossenv,這是一個利用「拼寫劫持」(typosquatting)技術的惡意套件,其名稱與流行的 cross-env 套件非常相似 。 它的惡意腳本會竊取使用者的環境變數,再次證明了即便安裝 npm 套件的不是 root 使用者,攻擊者仍能存取敏感資訊 。
最後一種威脅是依賴混淆(dependency confusion),這是一種利用私有和公共 npm 登錄檔的攻擊手法。 當開發者使用與內部套件同名的公共惡意套件時,套件管理器可能會錯誤地從 公共登錄檔下載並安裝惡意版本 。
1.2. 現代惡意軟體的解剖學
現代惡意套件的設計越來越複雜,其目的在於逃避簡單的靜態程式碼分析。
這些惡意酬載(payloads)通常採用多層次的混淆技術,使其難以被發現和理解 。
例如,最近發現的一個竊取 Chrome 瀏覽器資訊的惡意軟體,其程式碼使用了多達70層的混淆,包括 JavaScript 程式碼混淆、使用 zlib 和 Base64 壓縮反轉陣列的 Python 腳本,以及多重 bz2 壓縮等 。
這種複雜的機制使得僅僅透過檢視原始碼來判斷套件的安全性幾乎是不可能的任務。攻擊者在安裝過程中,可以利用動態程式碼評估、下載並執行額外酬載、存取敏感檔案(如瀏覽器儲存的密碼快取)等方式,在使用者毫無察覺的情況下竊取資料 。
1.3. 看不見的攻擊面:傳遞性依賴項
npm 生態系統的巨大力量來自其龐大的依賴關係網。 然而,這也創造了一個巨大的、通常不被監控的攻擊面。
當開發者安裝一個直接依賴項時,npm install 命令會自動拉取該套件的所有依賴項,以及這些依賴項的依賴項,從而形成一個複雜的「傳遞性依賴」樹 。 這意味著一個簡單的 npm install 指令可能在後台安裝數百個套件,每個套件都來自不同的維護者,其中任 何一個都可能潛藏惡意程式碼 。 由於大多數開發者只會審查他們直接安裝的套件,這些深層嵌套的傳遞性依賴項成了攻擊者最隱蔽的滲透途徑。
2. 安全開發環境的基礎原則
建立一個安全的 npm 開發環境,不僅僅是使用正確的工具,更需要遵循一套核心的基礎原則。 這些原則是所有具體安全實踐的基石。
2.1. 最小權限原則
這條原則至關重要:應始終使用非 root 或非管理員帳號來執行 npm 命令 。
惡意腳本一旦執行,其權限範圍會與執行它的使用者相同。 如果惡意套件是在 root 權限下安裝的,它便可能對整個系統造成破壞性的影響,例如竊取系統憑證或在系統層級安裝後門。
而如果是在受限的使用者帳號下安裝,其危害範圍將被限制在該使用者擁有的權限範圍內。
2.2. 不可變性與確定性建置的重要性
在軟體供應鏈安全中,一個確定性建置(deterministic build)不僅僅是為了確保建置的可重現性,更是一項關鍵的安全控制 。 確定性建置意味著無論何時何 地執行建置,都會精確地安裝完全相同的依賴項版本,包括所有傳遞性依賴項。
package-lock.json 檔案在這一原則中扮演著核心角色 。 它記錄了專案依賴關係樹的精確快照,確保每次安裝都會獲得完全相同的依賴項組合,從而防止在無意中拉取到潛在的惡意更新 。
2.3. 安全至上思維
安全不應被視為一個事後檢查的清單,而應是一個持續的過程,貫穿整個軟體開發生命週期。 這要求開發者將思維從被動應對(例如在漏洞被公開披露後才執行 npm audit fix)轉變為主動防禦(例如在安裝前就審查套件,並在 CI/CD 流程中持續監控)。 這不僅是為了保護自己,也是為了保護使用你所發佈軟體的每一個人 。
3. 第一層:主動審查與盡職調查
在套件被下載到本地機器之前,進行審查是防止供應鏈攻擊的第一道防線。 這種主動式防禦比事後清理更為有效。
3.1. 評估套件聲譽與健康狀況
在執行 npm install 之前,對套件進行審查至關重要。
僅僅因為套件很流行並不代表它絕對安全,因為受歡迎的 套件也可能被入侵 。
一個全面的審查應考量多個維度:
- 維護者信任訊號: 應檢查套件的作者資訊。如果作者是已知的、信譽良好的公司(例如 Oracle),其可信度通常較高 。同時,應關注維護者清單是否為空、是否看起來可疑,或是否最近有權限變更 。
- 流行度與成熟度: 套件的流行度(每週下載量)和發佈時間是重要的指標 。較高的下載量和較長的歷史通常意味著該套件經過了更廣泛的社群審查,相對更為可靠 。然而,不應過度依賴這些指標,因為一個成熟的套件同樣可能被入侵 。低於1000次下載量、最近發佈的版本號突然大幅跳躍(例如從1.2.3到15.0.0)且沒有清晰變更日誌的套件應被視為危險訊號 。
- 專案健康狀況: 檢查套件的原始碼庫(通常是 GitHub)可以提供關鍵資訊。一個健康的專案應該有持續的提交記錄、合理的議題解決率、清晰的 README 檔案和 LICENSE 檔案 。
3.2. 程式碼審查與預先安裝工具
除了上述的人工審查,自動化工具可以極大簡化此流程。
- 手動程式碼審查: 檢查 package.json 中的生命週期腳本是至關重要的一步 。如果發現 postinstall 等腳本,應手動檢查其內容,以確保它沒有執行惡意命令。由於現代惡意軟體常使用多層次混淆 ,這項任務可能極具挑戰性。
- 預先安裝工具: npq(npm package querier)是一個專為此目的設計的命令列工具 。它在套件安裝前自動執行一系列「理智檢查」(sanity checks),並提供一個互動式提示,詢 問使用者是否繼續安裝 。這些檢查包括:
- 查詢公開披露的漏洞資料庫(例如 Snyk.io) 。
- 檢查套件在 npm 上的發佈年齡(例如,少於22天會發出警告)。
- 評估下載量作為流行度指標 。
- 檢查是否存在 README 或 LICENSE 檔案 。
- 特別重要的是,它會檢查套件是否包含 pre/postinstall 等生命週期腳本,並將其作為潛在的危險訊號標示出來 。
npq 的核心價值在於,它將安全檢查的時間點前移至安裝前,這與 npm audit 的安裝後檢查截然不同 。 npm audit 僅檢查已知漏洞,但無法捕捉到全新的、尚未被收錄的零日惡意套件。 而 npq 則透過其合成檢查(synthetic checks)來評估套件的行為特徵,即使是全新的惡意套件,也可能因其低下載量、發佈時間短或存在可疑腳本而被標記,從而在惡意程式碼執行前就給予開發者警告。
4. 第二層:安全安裝時實踐
即使進行了預先審查,在實際安裝過程中仍需採取嚴格的控制措施,以確保建置的確定性與安全性。
4.1. 鎖定檔案的關鍵作用
在 Node.js 專案中,package-lock.json 檔案是確保供應鏈安全的核心 。 這個檔案記錄了專案所使用的每一個依賴項及其傳遞性依賴項的精確版本,以及用於驗證其完整性的 SHA-512 雜湊值 。
這解決了兩 個關鍵安全問題:
- 防止惡意修補版本(patch versions): npm 遵循語意化版本(SemVer)規範。版本號中的 ^ 符號允許自動更新次要版本和修補版本,而 ~ 符號則允許自動更新修補版本 。攻擊者可以發佈一個包含惡意程式碼的修補版本(例如從 1.2.3 到 1.2.4),如果 package.json 中的依賴項版本範圍是 ^1.2.3,那麼下一次 npm install 就可能在無意中拉取到這個惡意版本 。package-lock.json 透過鎖定精確版本來防止此類自動更新,確保每次安裝都與上次相同,從而成為專案的「安全錨點」 。
- 防止完整性被破壞: 儘管 npm 的鎖定檔案會對下載的依賴項進行完整性檢查,但舊版或配置不當的鎖定檔案可能使用較弱的雜湊演算法(例如 SHA-1),使其容易受到碰撞攻擊 。這意味著攻擊者可以創建一個具有與合法套件相同雜湊值的惡意套件,從而繞過驗證 。因此,應確保 npm 工具保持最新,並刪除舊的 package-lock.json 檔案以重新生成使用 SHA-512 雜湊的新檔案 。
4.2. $ npm ci 與 $ npm install
在安全實踐中,區分 $ npm ci 和 $ npm install 的用途是至關重要的 。 這兩個命令的行為模式截然不同,對供應鏈安全產生了深遠影響。
- $ npm install: 該命令的預設行為是安裝 package.json 中定義的依賴項。如果 package-lock.json 檔案存在,它會遵循其中的版本號。然而,如果 package.json 中的版本範圍允許更新(例如使用 ^ 或 ~),npm install 可以在安裝過程中更新 package-lock.json 到更新且兼容的版本 。這種行為雖然方便,但也意味著建置是非確定性的, 可能引入未經審查的新版本,從而為惡意更新打開大門 。
- $ npm ci: ci 代表「乾淨安裝」(clean install)或「持續整合」(continuous integration)。這個命令被視為安全與可重現性的黃金標準 。它有幾個關鍵的安全特性:
- 嚴格的確定性: $ npm ci 嚴格依賴於 package-lock.json。如果鎖定檔案與 package.json 中的依賴項不匹配,npm ci 會直接拋出錯誤並退出,而不是更新鎖定檔案 。這種失敗是一種積極的安全訊號,能及時警告開發者版本不一致的情況,防止惡意注入。
- 原子性與清潔性: 在開始安裝前,$ npm ci 會自動刪除 node_modules 資料夾,以確保一個全新的、乾淨的安裝環境 。
- 不寫入: $ npm ci 永遠不會修改 package.json 或 package-lock.json,這使得建置狀態始終保持「凍結」,確保了在 CI/CD 和生產環境中的一致性 。
總結來說,一個理想的實踐是:在本地開發時,使用 $ npm install 來添加和更新依賴項,並定期執行 npm update 來保持最新。而在所有 CI/CD 管道、生產建置和 Dockerfile 中,則應始終使用 $ npm ci 。 下表總結了這兩個命令的核心區別:
特徵 | $ npm install | $ npm ci |
---|---|---|
主要用途 | 在本地開發中添加或更新套件 | 在 CI/CD 和生產環境中進行乾淨、確定的安裝 |
鎖定檔案要求 | 可選,但強烈推薦 | 必須存在 package-lock.json |
鎖定檔案處理 | 會在 package.json 允許的範圍內更新鎖定檔案 | 永遠不會修改鎖定檔案,若不匹配則拋出錯誤 |
node_modules 處理 | 在現有樹上進行增量更新 | 安裝前自動刪除並重新建立 |
確定性 | 非確定性,可能拉取到新版本 | 完全確定性 |
速度 | 通常較慢(除非快取命中) | 通常較快(因跳過多個檢查) |
4.3. --ignore-scripts 標誌
惡意生命週期腳本是供應鏈攻擊中最直接的向量之一 。 npm 提供了一個 --ignore-scripts 標誌,可以直接緩解這一威脅 。
這個標誌會阻止在安裝任何套件時執行 package.json 中定義的任何生命週期腳本 。 然而,這是一個需要權衡利弊的措施。 許多合法的套件,如 bcrypt、node-sass 或 sharp,都依賴 postinstall 腳本來編譯原生二進制檔案或執行其他必要的設置任務 。 如果盲目地全面啟用 --ignore-scripts,可能會導致這些套件功能失效 。這使得開發者面臨在功能與安全之間做出選擇的困境 。
一個平衡的建議是:將 ignore-scripts=true 作為專案或全域 npm 配置 (.npmrc) 的預設設置 。 這確保了在沒有明確允許的情況下,任何套件都無法在你的系統上執行腳本。 然後,對於那些你知道並信任、且必須執行生命週期腳本的套件,再單獨地、有意識地禁用此設置。開發者可以透過 $ npm install @lavamoat/preinstall-always-fail 這個測試套件來確認其環境是否已成功禁用腳本 。
4.4. 使用 $ npm audit 進行自動漏洞審查
$ npm audit 是一個內建的命令,用於審核專案依賴項中已知的安全漏洞 。 它會將 專案依賴項的描述提交給 npm 登錄檔,並從其安全諮詢資料庫中獲取一份漏洞報告。 報告會包含受影響的套件名稱、漏洞嚴重性、描述、路徑以及可能的修復命令 。
$ npm audit 的主要優點在於其便捷性。 它預設在 $ npm install 之後自動運行 ,開發者也可以手動執行它來獲取報告。
$ npm audit fix 命令甚至可以自動應用相容的修復,對於簡單的修補版本漏洞尤其有效 。 然而,npm audit 的功能有所局限。 它僅檢查已知的漏洞,無法偵測到惡意程式碼本身,也無法發現尚未被添加到資料庫中的新威脅 。 此外,npm audit fix 在處理需要主要版本更新或存在同儕依賴項衝突的漏洞時常常會失敗 。 因此,它應被視為一個基本的、補充性的安全工具,而非全面的解決方案。
5. 第三層:持續監控與安裝後維護
供應鏈攻擊不僅發生在安裝時,也可能在套件發佈後才被發現。 因此,持續的監控和主動的後續維護至關重要。
5.1. 可視化你的依賴關係樹
要有效管理依賴項,首先必須了解其複雜性。 專案的依賴關係樹,特別是那些深層嵌套的傳遞性依賴項,通常超出了人類的心智模型 。 可視化工具可以幫助開發者理解這些隱藏的關聯。
例如,npmgraph 是一個線上工具,可以生成 npm 模組的依賴關係圖 。
而像 dependency-tree 這樣的工具則可以生成專案所有依賴項的樹狀結構或列表,這對於理解哪些檔案依賴於哪些外部模組非常有幫助 。 透過可視化,開發者可以更容易地識別那些拉入大量子依賴項的可疑套件,從而更精準地評估潛在的風險 。
5.2. 在 CI/CD 中整合安全掃描
依賴項安全不應僅僅是在開發者的本地機器上進行的孤立任務。 將安全審查整合到 CI/CD(持續整合/持續部署)管道中,是確保整個開發流程安全的現代最佳實踐 。
許多 CI/CD 平台,如 GitLab 和 GitHub,都提供了內建的依賴項掃描功能 。
其工作原理如下:當開發者提交程式碼或發起合併請求時,安全掃描器會自動分析 package.json 和 package-lock.json 檔案,將其中包含的依賴項與其內部的安全諮詢資料庫進行比對 。 一旦發現已知漏洞,管道就會被標記或直接停止,從而防止有漏洞的程式碼進入生產環境 。 GitLab 的持續漏洞掃描(Continuous Vulnerability Scanning)服務進一步將這一理念提升,使其不僅在 CI 管道運行時掃描,還在背景持續監控專案的依賴項 。即使沒有新的程式碼提交,一旦新的漏洞被公開披露並添加到其資料庫中,該服務也會自動為受影響的專案創建漏洞報告 。這確保了即使是在靜態的專案中,開發者也能及時了解其面臨的新威脅。