Shai-Hulud 是一種會自我散播、感染 npm 套件的蠕蟲,屬於一種軟體供應鏈攻擊,會讓每個受害者成為進一步散播的幫手。它在套件安裝時執行,從機器上竊取 npm 權杖、GitHub 權杖與雲端金鑰,並利用 npm 權杖發布受害者其他套件的受感染版本。自 2025 年 9 月第一波攻擊以來,它一再捲土重來,而作者在 2026 年 5 月公開了原始碼,因此之後的每一波攻擊都可能來自不同的團體。如果您的任何建置安裝過受感染的版本,請將該作業視為已遭入侵,並輪替它能讀取的每一個機密。
大部分的損害發生在 CI 中。在 2025 年 11 月那一波攻擊中,Wiz 發現只有 23% 的感染發生在開發者機器上,其餘大多在 CI/CD 管線中,其中又以 GitHub Actions 最為常見。一次建置會安裝數百個套件,持有發布與部署所需的機密,並在無人監看的情況下執行每一個安裝腳本。因此每當有新一波攻擊被公布,每個團隊都會問同一個問題:我們的建置有沒有安裝到受感染的版本?
發生了什麼事
| 時間 | 攻擊波次 | 有何不同 |
|---|---|---|
| 2025 年 9 月 | Shai-Hulud | 一個 postinstall 腳本執行了 3.6 MB 的 bundle.js,利用 TruffleHog 尋找機密,並重新發布維護者最多 20 個套件。超過 500 個套件受到影響,包括 @ctrl/tinycolor 與 18 個 @crowdstrike 套件。CISA 於 9 月 23 日發布了警示。 |
| 2025 年 11 月 | Sha1-Hulud: The Second Coming | 改用 preinstall,安裝 Bun 執行環境來執行 10 MB 的惡意酬載,在每位維護者最多 100 個套件中植入後門,並將受感染的機器註冊為 GitHub 自架執行器。Zapier、ENS、PostHog 與 Postman 的套件都遭到波及;Wiz 統計有超過 25,000 個儲存庫的機密外洩。 |
| 2025 年 12 月 | 「3.0」測試 | 單一套件中出現修改過的版本,沒有明顯散播。 |
| 2026 年 5 月 | TanStack | 42 個 @tanstack 套件的 84 個惡意版本。攻擊者從未竊取 npm 權杖:一個 pull_request_target 工作流程、一個遭汙染的 Actions 快取,以及從執行器記憶體讀取的 OIDC 權杖,讓他們能透過 TanStack 自己的 trusted publishing 發布。追蹤編號為 CVE-2026-45321,並於 5 月 27 日列入 CISA 的 KEV 目錄。 |
| 2026 年 5 月 | AntV | 323 個套件的 639 個版本。在 GitHub Actions 的 Linux 執行器上,惡意酬載從執行器程序的記憶體讀取機密,繞過日誌遮罩。GitHub 撤銷了 61,274 個 npm 寫入權杖。 |
| 2026 年 6 月 | Miasma | 32 個 @redhat-cloud-services 套件,透過注入的工作流程以有效的來源證明發布。每次感染都帶有獨立加密的惡意酬載,使以雜湊值為基礎的偵測失效。 |
| 2026 年 8 月 | CHAINDROP | keyv、cacheable 與 cache-manager 等熱門快取套件,總計數百個套件受到影響。它透過儲存庫中的編輯器與 AI 助理設定檔持續潛伏。 |
| 2026 年 9 月 | 再度出現 | 9 月 7 日,惡意酬載再度出現在四個套件中,這些套件躲過了 npm 新推出的發布時惡意軟體掃描。 |
| 2026 年 10 月 | tensorlake | 10 月 8 日,帶有蠕蟲的 [email protected] 被發布。OX Security 發現了新的攻擊者金鑰,並認為這可能是「獨立的威脅行為者,或是使用 Shai-Hulud 名號的新團體」。 |
蠕蟲在 CI 作業中做了什麼
細節在各波攻擊之間有所不同,但模式一致:
- 在安裝時執行。
preinstall或postinstall腳本會在npm install或npm ci處理到受感染的版本時立即執行。您的程式碼完全不需要匯入它。 - 蒐集機密。 環境變數、
.npmrc權杖、從中繼資料服務與機密管理工具取得的雲端憑證,以及 TruffleHog 在磁碟上找到的任何東西。後來的攻擊會直接讀取 GitHub Actions 執行器的記憶體,取得從未提供給該步驟、且在日誌中會被遮罩隱藏的機密。 - 將機密送出。 早期的攻擊會建立以蠕蟲命名的公開 GitHub 儲存庫,並推送一個工作流程,將儲存庫的每個機密傳送到
webhook.site。後來的攻擊則使用攻擊者的伺服器與即時通訊網路。 - 散播並潛伏。 利用竊得的 npm 權杖,它會發布其他套件的受感染版本。利用 GitHub 權杖,它會新增工作流程、註冊自架執行器,或植入在下次有人開啟儲存庫時就會執行的設定檔。
- 在 CI 中的行為不同。 2025 年 11 月的惡意酬載會檢查
GITHUB_ACTIONS、BUILDKITE、CIRCLE_SHA1等變數,在 CI 中以同步方式執行,在建置繼續之前完成它的工作。
誰會受到影響
任何在受感染版本仍存在於登錄檔期間安裝過它的機器或建置:開發者筆電、CI 作業、執行 npm install 的 Docker 建置,以及自架執行器。您不需要直接相依於該套件;遞移相依套件或以 npx 執行的工具就足以中招。每一波攻擊都有自己的套件與版本清單,而受感染的版本通常會在數小時到數天內被移除,因此關鍵在於該期間內是否有任何東西安裝了它們。
運作方式
Shai-Hulud 不是 npm 的錯誤。它是發布在原本合法之套件中的惡意程式碼,GitHub 針對受感染套件發布的惡意軟體資安公告將其歸類為嵌入式惡意程式碼(CWE-506)。每一波攻擊都結合了幾種手法:
- 安裝時執行。 npm 會在安裝期間,以使用者或 CI 作業的權限執行套件的生命週期腳本,例如
preinstall、postinstall與prepare。受感染的版本只要出現在相依樹中就夠了,不需要任何東西匯入它。 - 以竊得的發布權限散播。 利用從受害者取得的 npm 或 GitHub 權杖,蠕蟲會發布該受害者其他套件加入惡意酬載的新版本,因此每多一位受感染的維護者,就會感染更多套件。
- 透過受害者自己的發布管線發布(TanStack,2026 年 5 月)。 根據 GitHub 的資安公告,攻擊者串連了一個
pull_request_target工作流程、一個跨越 fork 與 base 信任邊界而遭汙染的 GitHub Actions 快取,以及從執行器記憶體讀取的 OIDC 權杖,在未變更發布工作流程的情況下,透過 TanStack 的 trusted publishing 發布。受感染的版本宣告了一個指向 fork 中某個 commit 的選用相依套件,其prepare腳本執行惡意酬載後刻意失敗,於是 npm 將其捨棄,在node_modules中不留痕跡。 - 有效的來源證明(Miasma,2026 年 6 月)。 注入的工作流程從受害者自己的儲存庫發布,因此受感染的版本帶有有效的來源證明,而每次感染各自加密的惡意酬載也讓以雜湊值為基礎的偵測失效。
如何檢查您是否受到影響
- 取得該波攻擊的受影響版本清單。 資安廠商會在數小時內公布清單;例如 Datadog 維護了一個包含套件與版本清單的入侵指標儲存庫。
- 搜尋每一個 lockfile,範圍是該期間內執行過 CI 的每個分支,而不只是預設分支。
- 搜尋建置本身。 在 CI 日誌中尋找
setup_bun.js、bun_environment.js或資安公告中提到的其他檔名,以及提及受影響版本的安裝輸出。如果您為每次建置保留了 SBOM,請在其中搜尋這些版本。 - 檢查您的 GitHub 組織,看看是否有以蠕蟲命名的新儲存庫、不是您撰寫的工作流程檔案(例如
shai-hulud-workflow.yml、discussion.yaml或formatter_*.yml),以及不是您註冊的自架執行器。
以下是可調整使用的指令。若要搜尋每個遠端分支的 lockfile,請將該波攻擊的受影響版本寫入 affected.txt,每行一個 name@version:
git fetch --all --quiet
for ref in $(git for-each-ref --format='%(refname:short)' refs/remotes); do
while read -r pkg; do
name=${pkg%@*}; version=${pkg##*@}
git grep -n -A3 -F "$name" "$ref" -- '*package-lock.json' '*yarn.lock' '*pnpm-lock.yaml' \
| grep -F "$version" | sed "s|^|$pkg |"
done < affected.txt
done
若要列出組織的自架執行器、提及蠕蟲的新儲存庫,以及已知由蠕蟲建立的工作流程檔案(請替換 ORG):
gh api --paginate orgs/ORG/actions/runners --jq '.runners[] | [.name, .os, .status] | @tsv'
gh repo list ORG --limit 1000 --json name,description,createdAt \
--jq '.[] | select((.description // "") | test("hulud"; "i")) | [.name, .createdAt] | @tsv'
for repo in $(gh repo list ORG --limit 1000 --json name --jq '.[].name'); do
gh api "repos/ORG/$repo/contents/.github/workflows" --jq '.[].name' 2>/dev/null \
| grep -E '^(shai-hulud-workflow\.yml|discussion\.yaml|formatter_.*\.yml)$' \
| sed "s|^|$repo |"
done
儲存庫也可能有自己的執行器(gh api repos/ORG/REPO/actions/runners),而早期的攻擊會在權杖遭竊之開發者的個人帳號中建立儲存庫,因此請您的開發者檢查他們自己的帳號。
為什麼光看 lockfile 不足以下定論
標準建議是在 package-lock.json、yarn.lock 或 pnpm-lock.yaml 中搜尋受影響的版本。請先這麼做,但乾淨的 lockfile 並不能證明建置是乾淨的:
- 在 lockfile 之外安裝的套件。
npx some-tool會將不屬於專案相依套件的套件安裝到 npm 快取中。設定步驟中的npm install -g、在建置腳本中安裝的工具,以及 Dockerfile 中的npm install,都會繞過專案的 lockfile。 - 分支上現在的 lockfile,不是當時執行的那份。 三週前的建置使用的是當時的 lockfile,以及觸發它的那個分支上的內容。相依套件更新的 pull request 會在任何人審查之前,就以新版本執行 CI。
- 快取會還原先前安裝的內容。 還原的
node_modules或 npm 快取,可能包含目前 lockfile 已不再提到的版本。 npm ci會安裝 lockfile 所寫的內容。 如果受感染的版本早已被鎖定,嚴格安裝就會忠實地把它裝進來。- 正確發布的版本仍可能是惡意的。 TanStack 與 Miasma 都是透過自己的發布工作流程遭到入侵,而 Miasma 的受感染版本還帶有有效的來源證明。確認版本是正確發布的,並不能說明它是乾淨的。
您真正需要的,是在受感染版本存在於登錄檔的整段期間內,跨越每個儲存庫與每個分支、記錄每次建置擷取了哪些套件版本的紀錄。請參閱我們是否受到影響?如何跨建置評估新的 CVE。
如何修正,以及暫時無法修補時該怎麼做
- 如果有任何建置安裝了受影響的版本,請將該作業視為已遭入侵。 輪替它能讀取的每一個機密:npm 權杖、GitHub 權杖、雲端憑證與部署金鑰。由於後來的攻擊會將竊得的機密發布到其他受害者的帳號中,請不要因為自己的帳號中沒有出現任何東西,就認為自己安全無虞。
- 清除快取。 刪除 GitHub Actions 快取,以及開發者機器與自架執行器上的 npm 快取,讓下一次建置安裝乾淨的版本:
gh cache delete --all --repo ORG/REPO # GitHub Actions caches, per repository
npm cache clean --force
rm -rf node_modules "$(pnpm store path)" # pnpm; for Yarn: yarn cache clean
- 固定到乾淨的版本,並移除蠕蟲新增的工作流程、儲存庫、執行器與設定檔。
驗證清除結果
- 每個 lockfile 都解析到乾淨的版本。 在每個分支上,
npm ls <package>(或pnpm why或yarn why)會顯示已安裝的版本;其中不得有任何版本出現在該波攻擊的清單上。 - 乾淨的安裝不含惡意酬載檔案。 完成上述快取清除後,重新安裝,並尋找資安公告中提到的惡意酬載檔案:
find node_modules -name bundle.js -size +3M \
-o -name setup_bun.js -o -name bun_environment.js -o -name router_init.js
- 舊的憑證已無法使用。 已撤銷的 npm 權杖不再出現在
npm token list中,舊的 GitHub 權杖與部署金鑰已刪除,雲端金鑰已停用,而不只是被替換。 - 快取已清除。 在每個受影響的儲存庫中,
gh cache list都不會顯示清除前建立的快取。 - 蠕蟲新增的東西都已移除: 沒有不是您建立的儲存庫、工作流程檔案、自架執行器,或編輯器與 AI 助理設定檔。
- 下一次建置是乾淨的。 從建置日誌或每次建置的 SBOM 檢查它實際安裝的版本,而不只是看 lockfile。
如何在下次降低損害
- 盡可能關閉安裝腳本。 2026 年 7 月發布的 npm v12 預設停用安裝腳本,並允許您按套件個別啟用。pnpm 與 Yarn 也提供類似的控制。
- 採用新版本前先等待。 大多數受感染的版本會在數小時到數天內被發現並移除。為相依套件更新設定冷卻期(Dependabot 與 Renovate 都支援),就能讓它們進不了您的建置。GitHub 的套件防火牆自 2026 年 10 月 20 日起在 GitHub Enterprise Cloud 公開預覽,會檢查 npm 安裝中是否有惡意套件,也提供冷卻期功能。
- 以 trusted publishing 與短期權杖發布。 npm 已於 2025 年 12 月撤銷所有傳統權杖。請改用 CI 中以 OIDC 為基礎的 trusted publishing,取代長期有效的權杖,並將發布工作流程與建置及測試工作流程分開。
- 強化蠕蟲濫用的工作流程。 避免使用
pull_request_target,將 action 固定到 commit SHA,並只給GITHUB_TOKEN每個作業所需的權限。請參閱 GitHub Actions 機密:如何外洩以及如何鎖定。 - 限制建置可以連線的對象。 建置需要的是它的套件登錄檔與少數幾個 API。如果它只能連線到這些地方,蠕蟲對
webhook.site或其自有伺服器的呼叫就會失敗。請參閱對外連線政策如何阻止從 GitHub Actions 竊取資料。這無法阻止透過建置合法使用的主機(例如 GitHub 自己的 API)竊取資料,而部分攻擊正是仰賴這種方式。
檢查是否遭到入侵
Shai-Hulud 本質上就是惡意的:任何安裝過受感染版本的機器或作業都已執行過它。入侵指標因攻擊波次而異,因此請使用您正在檢查之波次的資安公告。以下來自第一手資料:
| 攻擊波次 | 入侵指標 | 來源 |
|---|---|---|
| 2025 年 9 月 | 惡意酬載 bundle.js,SHA-256 46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09;工作流程 .github/workflows/shai-hulud-workflow.yml,位於分支 shai-hulud;名為 Shai-Hulud 的公開儲存庫;連線到 webhook.site | StepSecurity, CISA |
| 2025 年 11 月 | setup_bun.js,SHA-1 d1829b4708126dcc7bea7437c04d1f10eacd4a16;bun_environment.js,SHA-1 d60ec97eea19fffb4809bc35b91033b52490ca11 或 3d7570d14d34b0ba137d502f042b27b0f37a59fa;含有 cloud.json、contents.json、environment.json、truffleSecrets.json 與 actionsSecrets.json 的儲存庫;工作流程 .github/workflows/discussion.yaml 與 formatter_*.yml;名為 SHA1HULUD 的自架執行器 | Wiz |
| 2026 年 5 月,TanStack | "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c",位於 optionalDependencies 中;套件根目錄的惡意酬載 router_init.js(約 2.3 MB);輔助程式 tanstack_runner.js;連線到 filev2.getsession.org 以及 seed1.getsession.org 至 seed3.getsession.org,並從 litter.catbox.moe 下載第二階段酬載 | GHSA-g7cv-rxg3-hmpx |
後來的攻擊會在每次感染時改變惡意酬載,因此檔案雜湊值不符並不能證明任何事;請先比對套件版本。
何時應假設已遭入侵。 將每一台安裝過受影響版本的主機與每一個 CI 作業都視為已遭入侵,即使安裝失敗或在 node_modules 中沒有留下任何東西也一樣。GitHub 針對 TanStack 的資安公告指出,任何安裝過受影響版本的開發者或 CI 環境「都應視為已遭入侵」。請從受感染版本首次發布時開始檢查,而不是從該波攻擊被公布的那天開始。
輪替憑證。 輪替安裝過程能觸及的一切。根據事後檢討報告,在 TanStack 那一波攻擊中,這包括 AWS、GCP、Kubernetes、Vault、GitHub、npm 與 SSH 憑證。請檢視雲端稽核日誌中,受影響主機在安裝期間及之後的活動。
清除快取與建置產物。 依照 CISA 的建議,在建置產物儲存庫與相依套件代理伺服器中搜尋受影響版本的快取副本並加以刪除。刪除 node_modules、npm 快取與 CI 快取;在 TanStack 那一波攻擊中,攻擊者的入侵途徑是遭汙染的 pnpm store 快取,因此除了套件快取之外,也請檢查 Actions 快取。重新建置並重新發布任何由已遭入侵作業產出的東西。
該如何告知您的客戶
如果您出貨的軟體是以 npm 建置的,客戶會問您的產品是否帶有這隻蠕蟲,或您的發布管線是否遭到入侵。請以您檢查了什麼、做了什麼來回答:
我們已將 [產品] 在 [日期] 至 [日期] 之間的每一次建置,與已公布的 Shai-Hulud 受感染 npm 套件版本清單進行比對。[沒有任何建置安裝過受影響的版本。] / [[日期] 的 [元件] 建置安裝了 [package@version];我們已輪替這些建置可存取的憑證,以乾淨的相依套件重新建置 [產品],並發布了 [版本]。[x 至 y] 版受到影響;請升級至 [版本]。] 若新的攻擊波次改變了情況,我們將更新本通知。
如果您仍在檢查中,請如實告知,並說明何時會給出答覆。第一份暫行聲明與各種結果的範本,請參閱供應鏈攻擊後如何回應客戶。
CRACI 如何協助
CRACI 在自己的執行器上執行您的 GitHub Actions 作業(runs-on: craci),並記錄每個作業實際從 npm 與其他登錄檔擷取的每一個套件,包括從快取還原的套件。這份紀錄涵蓋了上述的盲點:npx 工具、全域安裝與建置腳本中的安裝都會出現,因為紀錄來自作業下載了什麼,而不是 lockfile 宣告了什麼。當新一波攻擊被公布時,您可以直接在 CRACI 記錄的建置中查詢受影響的版本,而不必從 lockfile 與日誌重建,而且每個作業的網路追蹤會顯示它連線到哪裡。建置歷程會保留每一次過去的建置及其 SBOM,因此您能在保留期限內看到哪些建置含有受感染的版本,以及您曝險的期間。
CRACI 的對外連線政策會限制作業能連線的對象,在作業開始前驗證並失效即封鎖(fail closed),並在作業嘗試連線到政策不允許的目的地時向您發出警示。已有公開 CVE 的受感染版本(例如 TanStack 那一波攻擊)會像其他漏洞一樣被標示,而政策關卡可以封鎖含有這些版本的建置。CRACI 不會為套件的惡意行為評分,也不會在資安公告出現之前標示新的受感染版本;如果您希望在安裝前就封鎖受感染的版本,請搭配套件信譽工具使用。
其他事件:所有漏洞。
資料來源與更新
第一手資料與原始研究:
- CISA:2025 年 9 月 23 日的警示、KEV 目錄
- GitHub:GHSA-g7cv-rxg3-hmpx(CVE-2026-45321)、打造更安全 npm 供應鏈的計畫、npm 傳統權杖已撤銷
- TanStack 事後檢討報告
- Wiz 對 2025 年 11 月攻擊的分析與其後續影響
- Microsoft 對 AntV 的分析與 Microsoft 的偵測指引
- Datadog 與 Elastic 對 CHAINDROP 的分析
- StepSecurity 對 2025 年 9 月攻擊的分析
- FIRST:CVE-2026-45321 的 EPSS 評分
更新紀錄:
- 2026-11-11:在資訊框中加入 EPSS、CWE 類別與致謝,加入可直接複製使用的檢查指令,並新增蠕蟲運作方式、驗證清除結果與檢查是否遭到入侵等章節,附上各波次的入侵指標。
- 2026-11-11:從部落格移至漏洞專區。加入 2026 年 10 月 8 日的 tensorlake 攻擊、2026 年 9 月再度出現的日期、TanStack 攻擊的 CVE-2026-45321 及其 KEV 列名,以及 GitHub 的套件防火牆。
- 2026-09-25:首次發布於 CRACI 部落格。