沒有人發布新版本
Wordfence 8 月 9 日關於 BdThemes 遭入侵的通報,描述的是一場沒有供應的供應鏈攻擊。沒有發布、沒有安裝腳本、沒有搶註名稱、外掛倉庫裡也沒有任何檔案被改動。攻擊者拿到了廠商儲存桶的寫入權限,改掉了一份 35 萬個網站在每次載入後台頁面時都會抓取的 JSON 資料。對開發者而言,這件事真正提出的問題是:跑著你的編碼代理的那台機器,被允許寫入什麼,又寫給誰。
你手上的每一項供應鏈管控,檢查的都是被發布出來的東西。這一次什麼都沒發布,卻照樣觸及了 35 萬個網站。
2026 年 8 月 7 日,Wordfence 開始接到通報:一些裝了 BdThemes 外掛的 WordPress 網站,長出了沒有人建立過的管理員帳號。名單一共七個外掛:Element Pack Addons for Elementor、Prime Slider、Pixel Gallery、Ultimate Post Kit、Ultimate Store Kit、Live Copy Paste 和 Smart Admin Assistant。光是 Element Pack 就有超過 10 萬次有效安裝,七個加起來超過 35 萬。
到了 8 月 8 日,廠商已清理自家 API,WordPress.org 則在調查期間下架了全部七個外掛。Wordfence 隔天發出 關於這次入侵的 PSA,而它最核心的發現是一個否定句:沒有人動過外掛的程式碼。
什麼都沒有被發布
BdThemes 的外掛裡帶著一個叫 Biggopti 的內部元件,工作既小又無聊:從廠商的遠端 API 抓取推廣橫幅,畫在 WordPress 的後台儀表板上。橫幅資料是一個放在 DigitalOcean Spaces 儲存桶裡的靜態 JSON 檔案,由 Cloudflare 後方供應。
攻擊者拿到了那個儲存桶的寫入權限。整場入侵就只有這樣。
Biggopti 把 JSON 回應裡的 display_id 欄位取出來,未經跳脫就丟進一個 HTML
的 id 屬性,這是一個 Wordfence 評為
CVSS 5.4、中等
的跨站腳本漏洞,源頭可追到 2026 年 3
月 1 日:某位開發者寫了一次,然後從一個外掛複製到下一個。因此,一個帶著
onanimationstart 處理器的 display_id
就是 JavaScript,而它會在每一位已登入管理員的瀏覽器裡、在每一次 wp-admin
頁面載入時、在每一個裝了該外掛的網站上執行。
酬載把這個座位用得很好。一個叫 w2.js
的腳本向命令與控制伺服器,也就是攻擊者用來告訴酬載哪些網站值得下手的那台機器,索取鎖定目標的指示,接著騎在真正管理員自己的工作階段和
nonce 上,透過 WordPress 的 REST
API 建立隱藏的管理員帳號。有一個變種會從網站的主機名稱推導出可預測的使用者名稱。一個假外掛,常見名稱像是
wp-smart-thumbnails,在 emer-run.php 送上一個 web
shell,也就是攻擊者只要載入一個普通網址就能操作的腳本。而 must-use
plugins,也就是 WordPress
每次請求都會載入、又不會顯示在外掛畫面上的那些,帶著一道魔術登入後門,以及一個藉由改寫後台顯示的使用者總數來掩蓋新帳號的模組。
被污染紀錄上的時間戳,把最早可能的起點放在 2026 年 6 月 23 日,比任何人察覺早了七週。
把這件事對照一個嚴謹團隊會跑的那些管控。版本鎖定:版本從未改變。鎖定檔與完整性雜湊:它們吻合,因為它們涵蓋的檔案根本沒被動過。安裝腳本審查:根本沒有安裝。搶註名稱偵測:名字對、廠商對、倉庫條目也對。版本之間差異的程式碼審查:沒有差異,因為沒有版本。憑證與 CDN:兩者都健康得很。Cloudflare 和 TLS 證明了那份 JSON 是從它本來就該來的儲存桶、原封不動地送達網站的,而這正是它好用的地方。
事件從一組能寫入的憑證開始
從廠商那一側讀這份通報,攻擊鏈就縮成一步。有人取得了一個儲存桶的寫入權限。之後的一切都是後果。
上游遭入侵的事件,一直以這個形狀出現。 Red Hat 的 npm scope 蠕蟲、 真的屬於 Microsoft 的那個倉庫、擴充套件市集、容器登錄檔:在每一件裡,被寫成報導的是酬載,而真正要緊的,是有人手上多了一組能覆寫別人按時抓取之產物的憑證。
這樣一組憑證住在哪裡?住在開發者的環境裡。shell 裡的
DIGITALOCEAN_ACCESS_TOKEN、~/.config 裡的 doctl
設定、~/.docker/config.json 裡的登錄檔登入、一份 AWS
設定檔、~/.git-credentials 裡的 GitHub token、一把能 push
的 SSH 金鑰。在筆電上、在終端機裡、在專案旁邊。而在 2026
年,專案旁邊也正是你跑編碼代理的地方。
這個部落格的多數文章,講的都是把那個代理反過來對付你的方法。一份 執行了指令的錯誤回報。一個 改寫了代理設定的網頁。一則 沒有人看得見的留言。一項 用你的身分執行的技能。 一場 Black Hat 演講裡的三種框架繞過。把整份清單當成已知,然後假設某個星期二,其中一種落到了你頭上。
那個星期二會有多糟,取決於一個你今天就能回答、而且不必知道哪一種注入會得手的問題:這台機器被允許寫入什麼,又寫給誰?在筆電上,答案是「所有你被允許寫入的東西」,而只要你有在發布任何東西,那就包括好幾十萬個陌生人不看一眼就抓下來的那份產物。
token 不在那台機器裡
Bromure Agentic Coding 在 Apple Silicon 上的一台可拋棄式 Linux VM 裡跑代理,而它流量的每一個位元組都會穿過主機上的一層代理伺服器, 在代理執行的那個盒子之外。Bromure 把憑證安排在那道邊界的外圍,而不是放進裡面。
把你的 DigitalOcean 個人存取權杖放進 Credentials 面板,Bromure
會把真值留在 Mac 上。落進 VM 的是個假的:一個 DIGITALOCEAN_ACCESS_TOKEN
環境變數,以及一份讓 doctl 不必 doctl auth init 就能運作的
~/.config/doctl/config.yaml,裡面裝的是佔位值。當 VM 對
api.digitalocean.com 發出請求時,主機上的代理伺服器會在線路上換成真的權杖,只針對那個目的地,別的一概不換。
在那台 VM 裡跑的任何東西都可以讀環境變數、grep
點檔案、走遍整顆磁碟:代理本身、一個相依套件、一個建置步驟、一行從被污染的議題裡跑出來的
shell 指令。它們收集到的是 brm_…。同一套安排也涵蓋其餘的部分:GitHub、GitLab
和 Bitbucket 的權杖,以假 base64 blob 形式寫在 ~/.docker/config.json
裡的容器登錄檔登入,一份帶著用完即丟客戶端憑證的合成 kubeconfig,Linear,各家資料庫的
HTTPS 端點,以及你在 Other API keys 底下另外加的任何東西。AWS
還更進一步:主機會用真正的密鑰以 SigV4 重新簽署每一個請求,所以繞過代理伺服器換來的是一個
InvalidSignatureException,而不是一次未經授權的寫入。
動詞由 Mac 上的 Guardrails 決定
這起事件只需要一個操作:覆寫儲存桶裡的一個物件。Guardrails
是主機代理伺服器內部的一套政策引擎,每種資源都有自己的模式:Off、Block
destructive 或 Read-only。把 DigitalOcean 設成 Read-only,每一次對
api.digitalocean.com 的變更都會換回一個硬邦邦的 403,而代理會把它當成一次普通的
API 失敗回報。Bromure 是在 macOS
裡做這個決定的。VM 裡沒有設定可以改、沒有環境變數可以取消、也沒有檔案可以編輯。
要發布,先問過
每一筆憑證都有 Require approval to use。把它打開,每一次假值→真值的替換,都會在真值上線路之前於主機端跳出一個同意對話框,而 SSH 那條路還有帶時限的授權:五分鐘、一小時、或這次工作階段的剩餘時間。推一次發布是你本來就打算做的事,代價是點一下。而一次你沒有發動過的寫入,會在你沒有預期的時刻,變成一個你沒有預期的對話框。
登錄檔和 git 也是同樣待遇
Guardrails 會針對登錄檔自己的主機名稱,用方法來分類容器登錄檔的流量:GET
和 HEAD 是 pull,PUT 和 POST 是 push,DELETE
是破壞性的。GitHub、GitLab 和 Bitbucket 在 REST API 和走 HTTPS 的 git
上都被涵蓋,在那裡 git-receive-pack 算作寫入,在 Read-only
模式下會被擋掉,而 fetch 永遠放行。一個讀得到你整張相依圖、卻什麼都推不出去的設定檔,只是兩個開關。
環境是一份你自己寫的清單
一個 Bromure 設定檔看得到的,是你分享給它的那些 Mac
資料夾,最多八個,各自掛載在 /home/ubuntu 底下。你 Mac
上的其他一切都沒有掛載、無法觸及、也無法列舉。憑證集合就是你加進那個設定檔的那些,所以一個你拿來寫前端的設定檔裡,根本沒有任何叢集憑證可供尋找,不管藏得多好都一樣。
執行期的抓取也一樣穿過代理伺服器
現在換到這個故事的下游那一側,也就是你是那 35 萬之一的那一側。你在那裡能抓到什麼,取決於你把檢查放在哪裡。
Bromure 的 Supply Chain 面板會篩檢跨 npm、PyPI、Cargo、RubyGems、Maven、NuGet、Go
模組和 Packagist 的套件抓取:一道預設開啟、會拒絕最近兩天內發布之物的時效門檻,OSV
查詢,把 socket.dev 或 Delpi 當作過濾供應者,會改寫 tarball
並修正登錄檔中繼資料雜湊好讓 npm
依然驗得過的安裝腳本剝除,以及在被鎖定檔釘住的 tarball 原封不動通過之前的一道詢問。VM
裡的 .npmrc 和 pip.conf 可以把這些規則收緊,但不能放寬。
這些規則之所以值回票價,在於 Bromure 執行它們的位置。主機上的代理伺服器是把規則套在請求上,而不是套在套件上,所以一個建置步驟、一支 CLI、一台 MCP 伺服器或應用程式本身在執行期從廠商儲存桶拉下的 JSON 檔案,穿過的是跟 tarball 同一道邊界。VM 對網路說的每一句話,都會穿過它。
這會改變那七週的樣子。把 Session trace 設在 Activity,Bromure 就會為 VM 發出的每一個請求記下主機、狀態、延遲、替換報告和任何外洩警告。設成 Everything,它連內容主體也一起留下,涵蓋每一個主機,用與你設定檔祕密相同的鑰匙圈金鑰加密,並可在 Trace Inspector 裡讀。你的建置所依賴的某份資料開始給出不一樣答案的那一天,就是一份你能打開來看的差異,在 Mac 上,在一份 VM 沒有寫入權的日誌裡。
一份被污染的資料,說到底也只是抵達一台機器的文字。當它以 tool_result
的形式、以檔案的形式、以一個抓下來的網頁的形式,或以 CLAUDE.md、AGENTS.md
這類指示檔案的形式抵達代理時,Prompt Injection
面板會用一個本機模型在裝置上為它評分,而且什麼都不會離開 Mac。選 Ask me what to
do,請求會暫停並秀出被標記的那一段;選 Block unilaterally,代理會拿到一個硬邦邦的
451。當你想讓這台機器徹底消失,Erase home 會重置 /home/ubuntu,而
Reset to base 會從基礎映像重新複製一份工作區的系統磁碟。
BdThemes 會把跳脫修好,儲存桶會換上更嚴的金鑰,而這個故事的下一個版本,會帶著另一家廠商和另一份資料出現。留得住的是底下那套安排。你的開發環境握著一把憑證,每一組都能改寫某個成千上萬人按時抓取的東西,而同一個環境如今還跑著一個整天在讀陌生人文字的代理。
這裡的多數文章問的是:你的代理應該被允許讀什麼。也請回答另一個問題,那個在筆電上讓人不安、在設定檔裡卻很容易的問題:它被允許寫入什麼,又寫給多少人?
安裝 Bromure Agentic Coding,給代理一台對你發布的任何東西都不握有鑰匙的機器。