返回所有文章
發佈於 · 作者 Renaud Deraison

那個套件真的是 Red Hat 的

2026 年 5 月底到 6 月 1 日之間,一個名為 Miasma 的蠕蟲把竊取憑證的程式碼推進了 @redhat-cloud-services npm scope 下的 32 個套件——這是 Red Hat 自己的命名空間,每週約 117,000 次下載,由 Red Hat 真實的發佈流水線簽名。沒有可以攔下的 typosquat,也沒有可以標記的未知維護者。信任訊號就是 scope 上廠商的名字,而廠商的名字正是攻擊者藉以混進來的東西。這裡說明為什麼「優先選擇信譽良好的發佈者」不再是一道防線,以及當執行安裝的代理跑在按設定檔隔離的 Bromure VM 裡時,什麼會改變。

我們在 5 月寫過的那些供應鏈攻擊都有破綻。本該是版本範圍的地方 出現了一個 Git URL。一個 Bun 執行階段憑空冒出來。一個故意失敗的 選擇性相依套件。Miasma 沒有破綻。那些套件是 @redhat-cloud-services/sources-client 和它的另外三十一個 手足——真真切切是 Red Hat 的,在 Red Hat 自己的 npm scope 之下, 由 Red Hat 自己的流水線發佈,帶著有效簽名。沒有任何東西是偽造的。 攻擊者不需要偽造任何東西。scope 上的那個名字本身就是整個漏洞。

一個正在解析 @redhat-cloud-services/vulnerabilities-client 的編碼代理不會猶豫,你也不會。它是來自業界最大型企業廠商之一的 第一方相依套件。沒有維護者需要審查,因為維護者就是 Red Hat。 沒有名字需要瞇起眼睛找有沒有字母調換,因為這個名字拼寫得 分毫不差。一個謹慎的開發者或謹慎的代理在執行 npm install 之前 所套用的每一條啟發法都回傳綠燈。於是安裝跑起來了,一個 preinstall 掛勾被觸發,而這個掛勾是一個 4.2 MB 大、混淆過的 JavaScript blob,它一開始就在掃描檔案系統找金鑰。

整起事件就是對一個令人不安的事實的示範:信譽良好的發佈者 不是一道資安控制。它感覺起來像。過去三年的供應鏈建議大多 仰賴它。而在 5 月 29 日,它換來的什麼也沒有。

Miasma 做了什麼。

這場攻擊行動——根據 OX Security, 字串 Miasma: The Spreading Blight 最早出現在一個日期為 2026 年 5 月 29 日的 commit 裡——由 Aikido 和 OX Security 捕獲,之後由 Socket、JFrog、Wiz、ReversingLabs、 Microsoft 等多家公司分析。BleepingComputerThe Hacker News 都在 6 月 1 日報導了這件事。

剝到只剩機制的樣貌:

  • 入口是一個被攻陷的 Red Hat 員工 GitHub 帳號,用來把惡意 commit 推進 @redhat-cloud-services 的原始碼儲存庫。
  • 一個 GitHub Actions 工作流程攜帶了一個 _index.js 指令稿,它 用一個 OIDC 權杖向 npm 的可信發佈端點認證——這正是 npm 現在 推薦取代長壽命發佈權杖的同一套無金鑰機制。從 npm 那一側看, Red Hat 的 CI 發佈了 Red Hat 的套件。簽名是真的。
  • 發佈出來的套件帶著一個 "preinstall": "node index.js" 掛勾, 以及一個大約 4.2 MB 的混淆酬載。
  • 一旦安裝,酬載就會搜刮 GitHub Actions secrets、AWS 憑證、 Google Cloud 憑證、Azure service principal、HashiCorp Vault 權杖、Kubernetes service-account 權杖、npm 與 PyPI 發佈權杖、 SSH 金鑰、Docker 憑證、GPG 金鑰,以及 .env 檔案——然後把 找到的任何東西加密後外洩。
  • 它利用竊得的存取權限透過 GitHub API 提交 commit 來自我傳播, 經由 GraphQL 讀取 action.yml 檔案,再透過 mutation 把新的 工作流程寫回去,使得這些變更在 Red Hat 自己的 commit 紀錄上, 用它自己的話來說,是已驗證已簽名的。

總計,32 個套件、橫跨 96 個版本遭殃,這些套件每週約有 117,000 次下載,而更廣泛的這場攻擊行動波及了 309 個 GitHub 儲存庫。Socket 的評估是,這*「實際上是一場迷你版的 Shai-Hulud 攻擊行動:它使用了同一套核心戰術——安裝期執行、 憑證搜刮、CI/CD 鎖定、加密外洩,以及潛在的下游傳播。」* Red Hat 的聲明是*「這些套件嚴格限於內部開發,惡意程式碼從未發佈供 客戶使用」*——這是真的,但對那些在套件被撤下之前的這段時間裡, 把 @redhat-cloud-services/* 當作傳遞相依套件拉進來的開發者和 CI runner 來說,也算不上多少安慰。

人人都推薦的那道防線,正是壞掉的那道。

我們在三週前寫過一個類似的 蠕蟲——TanStack 那次攻陷,破綻是一個掛在固定 Git URL 上的 prepare 指令稿,以及一個憑空冒出來的 Bun 執行階段。那篇文章 誠實的教訓是:別信任鎖定檔案,別信任簽名。Miasma 是同一根螺絲 的下一圈,而把哪裡不同說清楚是值得的,因為這個不同正是重點所在。

標準的供應鏈衛生堆疊有三層。固定你的版本。驗證 provenance。 優先選擇信譽良好的發佈者。Miasma 直接穿過這三層。固定毫無作用, 因為那些惡意版本就是發佈出來的版本。Provenance 毫無作用,因為 provenance 是有效的——OIDC 可信發佈流程貨真價實就是 Red Hat 的 CI,而在下游,蠕蟲鑄造出來的那些工作流程 commit 連 GitHub 自己 都標記為已驗證、已簽名。而第三層,優先選擇信譽良好的發佈者, 在這裡不只是被攻破——它就是攻擊面。@redhat-cloud-services scope 的信譽,正是這些套件被毫不遲疑地拉下來的原因。命名空間越受信任, 對接管它的人就越有用。

沒有任何一種「把套件讀得更仔細」的版本能逮到這個。套件沒問題。 套件是 Red Hat 的。問題在於以開發者周遭的環境憑證進行安裝期 程式碼執行才是那份合約,而一個受信任的名字對程式碼一旦執行 之後能碰到什麼,起不了任何改變。

開發者筆電 / CI RUNNER — 主機 secrets 對安裝跑的任何東西可見RED HAT 自己的流水線被攻陷的員工GitHub 帳號 → pushGH Actions: OIDC 發佈簽名: 有效provenance: 真實npm: @redhat-cloud-servicessources-clientvulnerabilities-clientrbac-client … (32 個套件)每週約 117k 次下載preinstall: node index.js編碼代理 — 沒有理由猶豫tool: bashnpm i @redhat-cloud-services/sources-client第一方廠商 scope ⇒ 綠燈↳ preinstall 執行 node index.js↳ 4.2 MB 混淆酬載主機檔案系統 & ENV — 全部真實,preinstall 掛勾全可讀~/.aws, GCP, Azure SPN雲端金鑰(真實)Vault 權杖, kube SA 權杖叢集存取(真實)~/.npmrc, PyPI 權杖在此發佈(真實)~/.ssh, GPG keyring簽署 + push(真實)$GITHUB_TOKEN, .env 檔案CI secrets(真實)tar | encrypt | exfiltrate自我傳播竊得的 GitHub 存取讀 action.yml (GraphQL)提交有毒工作流程顯示為: 已驗證, 已簽名行動波及 309 個儲存庫下一個 scope 繼承這份信任
Miasma 在一台直接解析 @redhat-cloud-services 的開發者機器或 CI runner 上的樣子。一個被攻陷的員工帳號推送到 Red Hat 的儲存庫;OIDC 可信發佈流程把惡意版本簽成貨真價實是 Red Hat 的;npm 帶著有效簽名供應它們。代理安裝一個第一方相依套件,沒有任何理由猶豫。preinstall 掛勾執行 node index.js——一個 4.2 MB 的 blob——它在主機上搜刮 AWS、GCP、Azure、Vault、Kubernetes、npm/PyPI 權杖、SSH 與 GPG 金鑰,以及 .env 檔案,加密後送出去,並利用竊得的 GitHub 存取權限提交全新的有毒工作流程,這些工作流程顯示為已驗證、已簽名。沒有 typosquat,沒有假維護者,沒有 Git-URL 把戲。信任訊號就是命名空間,而命名空間是真的。

同一次安裝,跑在 Bromure Agentic Coding 裡。

Bromure Agentic Coding 讓你的編碼代理跑在一個 按設定檔隔離的 Linux VM 裡——它有自己的核心、自己的檔案系統、 自己的網路堆疊,架在 Apple 的 Virtualization 框架上。一個設定檔 就是一個連貫的工作範圍:這個客戶這個內部產品這個開源 程式庫。代理在裡頭做它的 npm install,而主機——你真正的 keychain、你真正的雲端憑證、你真正的 SSH 金鑰——則在一道由硬體 強制執行、preinstall 掛勾無法跨越的邊界的另一側。

憑證不住在設定檔裡。它們住在主機上,藏在一個憑證仲介 之後。當代理需要 push 一個 commit 或發佈一個套件時,它不會從 訪客檔案系統上讀出一個權杖——根本沒有可讀的。它透過一個 Unix 網域通訊端,請求仲介代它使用一個憑證。仲介持有真正的 GitHub App 私鑰,鑄造一個短壽命、scope 限定到代理本就已在操作的那個儲存庫的 安裝權杖,並且——對於一個被設定為要求這麼做的設定檔——浮出一個 授權提示,由開發者在主機上回答之後請求才會送出去。權杖完全在 主機那一側被鑄造並掛到出站請求上;它從不進入訪客的記憶體或磁碟。 這個原則跟 ssh-agent 一樣老:仲介憑證的使用,絕不仲介它的取值

那麼把 Miasma 的搜刮走過這道邊界。node index.js~/.aws/credentials,找到一個 stub 或什麼都沒有。它讀 ~/.npmrc, 找不到發佈權杖。它在環境裡找 $GITHUB_TOKEN,找不到可偷的東西 ——安裝權杖在主機上鑄造、在主機上花用,從不寫進訪客,而且只有當 開發者回答了授權提示,仲介才會去鑄造一個。GPG keyring、SSH 私鑰、 Vault 權杖、kubeconfig:都在主機側,若有暴露也是經過仲介的, 若無就根本不存在。那個 4.2 MB 的酬載完全照設計跑到結束。它只是 外洩了一個從來就沒握著開發者金鑰的訪客。

代理在主機上npm i @redhat-cloud-services/sources-clientpreinstall → node index.js直接讀取主機真正的憑證 — 以明文讀取~/.aws/credentialsAKIA… 真實~/.npmrc _authTokennpm_… 真實$GITHUB_TOKENghp_… 真實~/.ssh/id_ed25519私鑰Vault, kube SA, GPG全部真實→ 加密 + 外洩→ 重新發佈, 傳播代理在按設定檔隔離的 BROMURE VM 裡訪客 — preinstall 掛勾能看到的東西~/.aws, ~/.npmrcstub / 不存在~/.ssh, GPG, Vault不在磁碟上need to push?→ 透過 Unix 通訊端問仲介授權提示 → 在主機側使用(「把金鑰給我」不是一個動詞)主機 — 仲介持有真正的金鑰GitHub App 私鑰在主機側鑄造短壽命權杖取值絕不跨越邊界
左邊:代理跑在主機上,所以 preinstall 掛勾直接讀到真正的金鑰——Miasma 鏈完成了。右邊:代理跑在一個按設定檔隔離的 Bromure VM 裡。真正的憑證坐在主機上、藏在一個仲介之後。當代理合法地需要 push 時,它透過一個 Unix 通訊端請求仲介,仲介鑄造一個短壽命、scope 限定到儲存庫的安裝權杖在主機側使用——並擋在一個由開發者在主機上回答的授權提示之後——而權杖從不落進訪客。那個惡意的 preinstall 掛勾,讀著訪客的檔案系統和環境,找到的是 stub,根本沒有任何權杖。爆炸半徑是一個設定檔,而不是開發者的整個 keychain。

proxy 拒絕轉發的那次 push。

竊取金鑰只是 Miasma 的一半。另一半是傳播:它利用竊得的 GitHub 存取權限,透過 API 把有毒的工作流程提交回去,而這正是把一次糟糕的 安裝變成 309 個儲存庫的東西。仲介處理竊取那一半。Guardrails, 在 Bromure Agentic Coding 2.0 中加入,處理濫用 那一半。

Guardrails 是一個主機側的政策引擎,它就住在被仲介的流量本就已經 流經的同一個 MITM proxy 裡,所以訪客裡一個被攻陷的代理無法繞過 它。每一個請求都依它對資源實際做了什麼來分類,而每一種資源—— GitHub、AWS、Kubernetes、Docker registry、DigitalOcean、GitLab、 Bitbucket、託管資料庫——都可以設成關閉攔截破壞性操作唯讀。把一個設定檔的 GitHub guardrail 設成唯讀模式,一次 git push——也就是蠕蟲寫回它的工作流程所需要的 git-receive-pack ——會回傳一個硬性的 403,而 git fetch 照常運作。對 Kubernetes API 的一次 DELETE、registry 裡一次 manifest 刪除、對 EC2 的一次 Terminate* 呼叫:一樣的處理。代理只看到一次普通的 API 失敗。 Miasma 的傳播步驟是一次 proxy 拒絕轉發的寫入,無論代理究竟有沒有 靠近過一個真正的憑證。

那持久化呢?

這裡正是按設定檔的模型對一項代價誠實以對的地方。Miasma 是一個 蠕蟲;它全部的野心就是回來。在一個可拋棄磁碟的幻想裡,你大手一揮 就把它揮掉了——任務結束後磁碟就沒了。一個 Bromure 設定檔是長期 存活的,所以一個把自己寫進設定檔內某個啟動指令稿的酬載,能夠 存活到那個設定檔的下一次代理工作階段。我們不打算假裝不是這樣。

不過那份持久化所繼承的,是一個沒有主機金鑰的訪客,以及一個只說 短壽命、scope 受限權杖的仲介。蠕蟲醒來時,還在它死掉時那同一個 沙箱裡。它能讀到同樣那些 stub。它能請仲介為這個設定檔被授權的 那唯一一個儲存庫使用一個憑證——而開發者仍然得回答那個授權提示, 這件事才能成事,而 Guardrails 大可直接拒絕那次 push。它搆不到 主機 keychain、其他設定檔,或開發者的雲端憑證,因為這些東西 一開始就從來不在那道邊界裡面。持久化買到的,是繼續存在於一個 空無一物的盒子裡。

而這其中的每一分——preinstall 被觸發、node index.js 載入一個 數百萬位元組的 blob、那個寫進啟動路徑的檔案、那次出口嘗試——都會 落進hypervisor 層級的工作階段追蹤。 當 Aikido 在隔天早上發佈指標時,「這個設定檔到底有沒有跑過 Miasma?」這個問題是一次 grep,而不是一場事件應變行動。

這在哪裡救不了你。

仲介的範圍就是整盤棋。

如果一個設定檔今天被佈建成可以發佈到你的 npm scope,而那個 設定檔今天安裝了 Miasma,仲介就會讓它發佈。仲介之所以有效, 是因為授權範圍既窄又短壽命。一個不需要發佈的設定檔,就不該 能夠發佈。刻意把範圍縮窄。

它不審查 diff。

Miasma 透過提交看起來已驗證、已簽名的工作流程來傳播。一個唯讀 的 GitHub guardrail 會直接攔下那次 push——但一個合法需要 push 的設定檔跑在攔截破壞性操作模式下,而 Guardrails 是依方法分類, 不是依 diff 裡有什麼。隔離也好、方法層級的 guardrail 也好,都 無法阻止一個代理被說服去提交一個它被允許 push 的有毒工作流程。 去讀那個 diff。追蹤會告訴你該讀哪些 diff。

剪貼簿預設是共用的。

Bromure 出廠時主機/訪客剪貼簿共用是開著的,因為把堆疊追蹤貼進 聊天是人類整天都在做的事。對於一個敏感的設定檔,把剪貼簿隔離。 這個控制存在;它只是不是預設值。

追蹤是稽核日誌,不是 IDS。

工作階段追蹤記錄了 preinstall、blob、出口。它本身並不會自行判定 目的地是惡意的。它擷取的東西足夠多,足以讓某人一旦點出那個指標, 你的答案就在兩秒之外。

下一個 scope 已經受信任了。

TanStack 蠕蟲的教訓是,鎖定檔案和簽名都不是防線。Miasma 補上了 那個令人不安的推論:發佈者也不是。@redhat-cloud-services scope 受信任並沒做錯什麼——受信任正是一個廠商命名空間存在的全部目的, 而這恰恰也是讓它值得攻擊的東西。下一場攻擊行動會藉著一個你同樣 信任、同樣有效簽名、由一個直到失守前都貨真價實是廠商自己的流水線 混進來的 scope。

你沒辦法靠更謹慎地信任來修好這個。你修好它的方式,是把事情安排成 讓「這是哪個 scope 發佈的」不再是你的 keychain 所仰賴的那個問題。 Bromure Agentic Coding 就是那種組態:代理在一個 按設定檔隔離的 VM 裡做它的安裝,真正的憑證藏在仲介之後留在主機上, 而一個 preinstall 掛勾最壞能做的,就是外洩一個從來沒握著你金鑰的 盒子。它免費、開源,今天就發行。