npm installpip install 看起來像是下載套件並解壓縮,安裝過程卻可能直接執行套件作者提供的程式碼。這些程式原本用來下載平台專用檔案、編譯原生模組或更新自動載入器(autoloader),但惡意套件也能利用同一條路徑讀取環境變數、搜尋 SSH 金鑰、竊取 npm token,甚至把資料傳到外部伺服器。

這類風險並非只有打錯套件名稱才會遇到。維護者帳號遭竊、正常套件發布伺服器被入侵,或 CI/CD workflow 使用的 GitHub Action 洩漏發布憑證,都可能讓專案在更新依賴套件或重建 lockfile 時收到惡意版本。axios 與 LiteLLM 的實際攻擊路徑可以參考 npm 與 pip 套件遭供應鏈攻擊 ,這篇文章則集中處理另一個問題:套件已經下載之後,怎麼避免它立刻取得執行程式碼的機會。

安裝套件不只是複製檔案

JavaScript 套件可以在 package.json 定義 preinstallinstallpostinstall。npm 安裝 Git 依賴套件時,還可能執行 prepareprepack 和其他建置腳本(build script)。這些生命週期腳本(lifecycle script)是一般 shell command,權限與執行套件管理器的帳號相同,也會繼承當下可讀取的環境變數。

postinstall 經常被討論,但它不是唯一入口。有 binding.gyp 的 Node.js 原生模組可能自動呼叫 node-gyp;pip 安裝原始碼發行檔(source distribution,簡稱 sdist)時需要啟動 Python 建置後端(build backend);Composer 外掛(plugin)則能在 Composer 執行期間載入 PHP 程式碼。Python 套件還能放入 .pth 檔案,其中以 import 開頭的內容會在往後每次啟動 Python 時執行。

因此防護不能只搜尋套件裡有沒有名為 postinstall 的欄位。比較完整的做法是把「下載檔案」「驗證來源」「執行建置」拆成不同階段,預設不執行第三方程式碼,只讓審查過的套件通過。

套件安裝過程從自動執行腳本取得環境權限,再造成憑證與檔案外洩的攻擊路徑
安裝腳本會在應用程式啟動前取得執行環境的權限。

各套件管理器的腳本控制方式

套件管理器全部停用個別核准需要留意的差異
npm 12--ignore-scriptsnpm approve-scripts依賴套件安裝腳本預設封鎖,可把核准綁定特定版本
npm 11 以前--ignore-scripts沒有 npm 12 的原生核准流程適合先全部停用,再於隔離階段處理必要建置
pnpmignoreScripts: truepnpm approve-builds核准結果寫入 pnpm-workspace.yaml
YarnenableScripts: falsedependenciesMeta[].built工作區自身的腳本與第三方依賴套件要分開看
Bun--ignore-scriptstrustedDependencies內建一份預設信任清單,可用空陣列完全取代
pip沒有對等的 no-scripts 選項沒有建置腳本白名單改用 wheel-only、雜湊與隔離環境避開原始碼建置
Composer--no-scripts --no-pluginsallow-plugins根專案腳本與外掛是兩條不同的執行路徑
Homebrew 6移除不需要的第三方 tapbrew trust優先信任單一 formula/cask,不要直接信任整個 tap
不同套件管理器需要採用不同的安裝期程式碼控制方式。

npm 12 的安裝腳本核准機制

npm 12 把依賴套件的安裝腳本改為預設封鎖。未出現在 allowScripts 政策中的依賴套件仍會安裝,但 npm 會略過它的安裝腳本,完成後列出哪些套件需要審查。這跟過去只能在「全部執行」與「全部停用」之間選擇相比,多了逐一核准的選項。

以下流程適合 npm 12 專案。npm install-scripts 是完整指令,較短的 npm approve-scriptsnpm deny-scripts 是它的別名:

# 依照 package-lock.json 安裝,未核准的 dependency script 不會執行
npm ci

# 列出仍待審查的安裝腳本,不修改設定
npm install-scripts ls

# 審查套件內容後,只核准確實需要建置的套件
npm install-scripts approve sharp esbuild

# 明確拒絕不應執行安裝腳本的套件
npm install-scripts deny suspicious-package

# 移除已經不在 dependency tree 裡的舊核准項目
npm install-scripts prune

核准結果會寫進專案的 package.json。npm 預設把核准綁定目前安裝的版本,例如審查的是 [email protected],套件日後升級便需要重新核准。不要為了省事加上 --no-allow-scripts-pin,也不建議直接執行 approve --all,否則白名單會失去審查的意義。

CI 對未審查腳本直接失敗

npm 12 預設略過未核准腳本並顯示警告,這對本機安裝比較溫和,放進 CI 卻可能掩蓋新的依賴套件。把下面設定提交到專案的 .npmrc,任何未審查的安裝腳本都會讓安裝失敗:

# 未列入 allowScripts 的安裝腳本視為錯誤
strict-allow-scripts=true

# 新版本發布七天後才允許解析,保留社群發現問題的時間
min-release-age=7

# Git 與任意遠端 tarball dependency 維持 npm 12 的封鎖預設
allow-git=none
allow-remote=none

min-release-age 不是惡意程式掃描器,只是讓專案不要第一時間取得剛發布的版本。若 npm audit fix 要安裝的修補版本被七天視窗擋住,npm 會保留舊版本、顯示警告並以非零狀態結束;一般安裝若找不到符合冷卻期的版本則會失敗。這時應針對該套件加入例外,而不是永久關掉整個政策。

--ignore-scripts 仍有使用時機

需要檢查陌生程式碼儲存庫(repository)、處理資安事件,或使用 npm 11 以前版本時,可以用 --ignore-scripts 完全關閉腳本:

# 單次安裝停用所有 package.json script
npm ci --ignore-scripts

# 將設定寫進專案的 .npmrc
npm config set ignore-scripts=true --location=project

這個選項不代表 npm 從此不能執行任何指令。明確呼叫 npm runnpm testnpm start 時,指定的腳本仍會執行,只是不再自動接上對應的 pre 與 post script。它也不會阻止應用程式日後 import 惡意套件,或阻止開發者執行套件提供的 CLI。

pnpm、Yarn 與 Bun 的白名單

pnpm approve-builds

pnpm 從 10.1 加入 pnpm approve-builds,現在的設定會把允許和拒絕結果寫入 pnpm-workspace.yamlallowBuilds。不帶參數會進入互動選擇,也能在 CI 設定變更前明確列出套件:

# 核准 esbuild、fsevents,拒絕 core-js 的安裝腳本
pnpm approve-builds esbuild fsevents !core-js

# 檢查哪些 dependency build 被略過
pnpm ignored-builds

專案若仍使用較舊的 pnpm,也能設定 ignoreScripts: true 完全停用。升級後應改用 allowBuilds 管理必要套件,避免為了少數原生模組開放整棵依賴樹。

Yarn dependenciesMeta

Yarn 可在 .yarnrc.yml 關閉第三方套件的 postinstall,再用根目錄 package.jsondependenciesMeta 開放個別套件:

# .yarnrc.yml
enableScripts: false
{
  "dependenciesMeta": {
    "sharp": {
      "built": true
    },
    "suspicious-package": {
      "built": false
    }
  }
}

enableScriptsfalse,只有明確標記 built: true 的套件可以建置。不過 Yarn 把專案裡的工作區(workspace)視為可信任程式碼,工作區自己的 postinstall 仍可能執行;審查外部 pull request 時不能把兩者混為一談。

Bun trustedDependencies

Bun 不會執行任意依賴套件的生命週期腳本,但內建一份常見套件的信任清單。如果專案希望完全掌握名單,可以在 package.json 明確設定;一旦出現 trustedDependencies,它會取代而不是附加到 Bun 的內建清單:

{
  "trustedDependencies": [
    "sharp",
    "esbuild"
  ]
}

如果專案完全不需要依賴套件腳本,設成空陣列即可連內建清單一起關閉。臨時檢查陌生專案時則使用 bun install --ignore-scripts

pip 沒有 --ignore-scripts

pip 的模型跟 npm 不同,沒有可以保證「安裝但不執行第三方程式碼」的對等開關。pip 官方文件提醒,預設安裝流程包含從發行檔(distribution)執行任意程式碼。尤其安裝 sdist 時,pip 需要啟動套件指定的建置後端來產生 wheel。

風險較低的做法是只接受已建好的 wheel,再用本地保存的雜湊(hash)驗證檔案:

# requirements.txt 需要固定所有直接與間接依賴套件的版本和 SHA-256
python -m pip install \
  --only-binary=:all: \
  --require-hashes \
  -r requirements.txt

--only-binary=:all: 會拒絕沒有 wheel 的套件,因此某些冷門套件或新平台可能無法安裝。這時不要退回在正式部署環境直接編譯 sdist,可以另外建立沒有正式憑證、限制外網的建置環境,把 sdist 轉成內部 wheel,審查後再由部署流程安裝。

requirements.txt 中的雜湊值若已經過審查並提交版控,可以確認下載檔案與核准時相同,但不能證明檔案本身沒有惡意行為。Python 的 wheel 也可能帶入含可執行行的 .pth,往後每次啟動 Python 都會處理。因此 wheel-only 與雜湊還要搭配隔離環境、套件內容審查及最小權限。

Composer 的 script 與 plugin 要分開關閉

Composer 有兩條需要控制的程式碼執行路徑。--no-scripts 只略過根專案 composer.json 定義的腳本,--no-plugins 才會停用 Composer 外掛。檢查陌生 PHP 專案時應一起使用:

composer install \
  --no-scripts \
  --no-plugins \
  --no-interaction

正式專案若確實需要外掛,可以透過 allow-plugins 逐一核准。Composer 2.2 之後預設值是空物件,不會直接載入未列出的外掛;互動模式會詢問,CI 則應把決定提交到 composer.json,不要在流程(pipeline)中臨時允許全部套件。

{
  "config": {
    "allow-plugins": {
      "composer/installers": true,
      "unknown-vendor/*": false
    }
  }
}

Homebrew 的 Tap Trust 與建置隔離

Homebrew 的 formula、cask 與 external command 不是單純的套件 metadata,其中可能包含 Homebrew 需要載入的 Ruby 程式碼,而且會以目前使用者的權限執行。第三方 tap 一旦遭入侵、轉移 repository 所有權,或出現同名套件,攻擊者便可能在 brew installbrew update 或解析依賴套件時取得程式碼執行機會。

Homebrew 6 預設要求非官方 tap 明確取得信任。安裝時應優先使用完整名稱,或只核准實際需要的 formula/cask,不要為了安裝一個工具就信任整個 tap:

# 使用完整名稱,只信任這一個 formula
brew install user/repository/formula

# 已經加入 tap 時,只核准指定 formula
brew trust --formula user/repository/formula
brew install formula

# 列出目前信任的項目
brew trust

# 列出未信任項目,或撤銷不再需要的信任
brew untrust
brew untrust --formula user/repository/formula

brew trust user/repository 會信任該 tap 現在與未來的所有 formula、cask 和 external command,範圍比單一 formula 大很多。環境變數 HOMEBREW_NO_REQUIRE_TAP_TRUST=1 則會關掉信任檢查,官方文件不建議使用,而且這個暫時的相容選項預計移除。

Bottle 與原始碼建置的差異

Homebrew 能取得預先編譯的 bottle 時,不必在本機執行完整編譯流程;沒有對應 bottle、指定 --build-from-source 或安裝自訂 formula 時,才會進入較複雜的建置階段。Homebrew 在 macOS 與 Linux 都有建置隔離機制,Homebrew 6 也為 Linux 加入 Bubblewrap sandbox。不過 sandbox 只能限制建置程式能接觸的檔案與系統資源,不能證明 formula、下載的原始碼或最後產物值得信任。

團隊用 Brewfile 建立開發環境時,應把第三方來源與信任範圍一起提交版控,並在更新前檢查 tap、formula URL 與 checksum 的差異。Homebrew 6 的 Tap Trust、Linux sandbox 及 CI 調整方式在 Homebrew 6.0 大改版重點 有更完整的整理。

CI 安裝階段的隔離設計

白名單可以減少自動執行,但無法取代隔離環境(sandbox)。核准過的套件日後仍可能被維護者帳號劫持,專案自己的建置腳本也可能因 pull request 被修改。CI 流程可以拆成四個階段:

套件依序通過固定版本、腳本白名單、無憑證隔離與限制外網的四層防護流程
未核准腳本在接觸憑證與外網之前就被攔下。
  1. 解析與下載:使用 lockfile,停用腳本,只允許連線到指定套件倉庫(registry)或內部鏡像(mirror)。
  2. 內容驗證:檢查 lockfile 差異、來源 URL、雜湊、簽章與新增的生命週期腳本。這個階段不提供發布 token。
  3. 核准建置:只執行白名單套件的必要建置,放在短命容器或 VM,檔案系統限制於工作區,外網預設關閉。
  4. 測試與發布:測試通過後才把產物交給有發布權限的工作(job)。安裝依賴套件的工作不應同時持有 npm、PyPI、GitHub 或雲端正式環境憑證。

容器隔離之外,也要確認安裝環境沒有掛載正式憑證或高權限介面。如果容器掛載了 SSH agent、Docker socket、使用者家目錄或長效雲端金鑰,惡意腳本不必逃出容器也能使用這些資源存取其他系統。

macOS、Windows 與 Linux 的出站連線攔截

開發機無法像 CI 一樣每次都重建短命容器,可以另外加入出站防火牆。三個平台都能封鎖向外連線,但內建功能與互動方式不完全相同:

平台內建能力適合開發機的工具實際差異
macOSApplication Firewall 主要管理進站連線LuLu、Little Snitch能依程式顯示或管理未知出站連線
WindowsWindows Defender Firewall 可依程式、服務、IP、protocol 與 port 建立出站規則Portmaster內建預設允許出站,需要自行建立封鎖規則;Portmaster 提供較直觀的應用程式連線監看
Linuxnftables/iptables 可建立出站政策OpenSnitch、PortmasterOpenSnitch 會把連線對應到 process,較接近 LuLu 的互動式操作
原生防火牆適合執行固定政策;應用程式防火牆較容易發現陌生安裝腳本突然連線。

macOS:LuLu 是免費開源工具,新程式建立尚未授權的出站連線時,會顯示發起連線的 process、完整路徑、程式碼簽章、process hierarchy 與遠端目的地,再讓使用者決定允許或阻擋。Little Snitch 是同類付費工具,另外提供較完整的歷史流量、傳輸量、伺服器位置與規則管理介面。

macOS 使用 LuLu 攔截陌生專案安裝腳本出站連線,檢查程式與遠端目的地後決定允許或阻擋
LuLu 能識別發起連線的程式與目的地,但不會解密 TLS 內容。

Windows:Windows Defender Firewall 本身就能針對特定執行檔建立 outbound blocking rule,也能依目的 IP、protocol 與 port 縮小範圍。不過 Windows 預設允許不符合封鎖規則的出站流量,不會像 LuLu 一樣詢問每個新程式。測試陌生套件時,可以在短命 VM 內改成預設封鎖出站,再只開放套件 registry;若需要依應用程式持續監看,Portmaster 同時支援 Windows 與部分 Linux distribution。

Linux:nftables、iptables 或 UFW 能設定出站 policy,適合伺服器與 CI 採用預設拒絕,再只開放內部 mirror、DNS 與必要服務。桌面開發機若想知道連線由哪個 binary 發起,可以使用免費開源的 OpenSnitch;它是受 Little Snitch 啟發的互動式 application firewall,能對新出站連線顯示提示並建立規則。

實際檢查陌生專案時,可以先以 --ignore-scripts 下載依賴套件,接著暫時封鎖 Node.js、Python、PowerShell 或 shell 的未知出站連線,再於隔離環境執行必要的建置腳本。如果安裝期間突然連到與官方 registry 無關的網域,即使還不能確認傳輸內容,也已經是需要停止檢查的訊號。

不過,上述工具通常只能確認「哪個程式連到哪裡、傳了多少資料」,不能直接判斷 HTTPS 加密內容裡是不是某個私鑰或原始碼檔案。Proxyman、Charles 或 mitmproxy 這類 HTTPS debugging proxy 可以在受控測試中安裝本機 CA、解密部分 HTTP 流量,但遇到 certificate pinning、非 HTTP protocol 或應用層再次加密仍看不到內容,也不適合當成全機長期 DLP。企業若要依檔案敏感度阻擋上傳,需要 Microsoft Purview Endpoint DLP 這類內容型資料外洩防護;個人開發機則先用應用程式防火牆阻擋未知目的地,最容易開始。

每一層防護擋住的風險不同

防護主要作用無法處理
lockfile固定解析到的版本與依賴樹已鎖定的版本本來就是惡意版本
雜湊/integrity確認檔案是否與核准時相同核准的原始檔本身含惡意程式
npm auditpip-audit找出資料庫已收錄的漏洞剛發布、尚未被回報的惡意套件
腳本白名單阻止安裝當下自動執行未核准程式碼應用程式稍後 import 或執行惡意套件
版本冷卻期避開剛發布就立刻擴散的攻擊潛伏多日或鎖定舊版本的攻擊
應用程式出站防火牆發現並阻擋安裝工具連到未知目的地直接判斷 TLS 內傳輸的是哪個檔案
無憑證、無外網隔離環境限制竊取資料與向外連線的影響自動判斷套件是否可信
沒有單一設定能解決供應鏈攻擊,重點是讓多層防護彼此補位。

團隊若只能先做一輪調整,可以從三件事開始:把 lockfile 與腳本核准清單提交進 Git、讓 CI 遇到未審查腳本直接失敗、把套件安裝移到沒有正式憑證的隔離工作。這三項不需要導入大型資安平台,能切斷常見的安裝期自動執行與資料外傳路徑。

常見問答

關閉安裝腳本會不會讓套件壞掉

可能會。有些套件需要下載平台專用 binary、呼叫 node-gyp 或產生額外檔案,略過腳本後可能到執行時才報錯。因此適合採用「預設封鎖、逐一核准」,而不是遇到第一個錯誤就對所有套件重新開放。

--ignore-scripts 能完全阻止惡意套件嗎

不能。它處理的是安裝期間的自動執行。應用程式之後 import 套件、執行套件 CLI,或主動呼叫 npm run 時,惡意程式仍可能啟動。腳本封鎖是縮小安裝期攻擊面,不是判定套件安全的掃描器。

使用 npm ci 還需要腳本白名單嗎

需要。npm ci 會依照既有 package-lock.json 安裝,且不會自動改寫依賴樹,但鎖定的套件仍可能含有安裝腳本。lockfile 解決可重現性,白名單控制程式碼執行權,兩者不能互相取代。

pip 安裝 wheel 就不會執行任何程式碼嗎

wheel 可以避開安裝當下從 sdist 執行建置後端,但不能保證套件內容安全。套件可能在 import 時執行惡意邏輯,也可能放入會由 Python site 模組處理的 .pth。因此 wheel-only 應搭配雜湊、隔離環境及依賴套件審查。

參考來源


Sponsored Links

發佈留言