建置把金鑰一起出貨了
Beacon CRM 的擴充版事件報告,把整份客戶資料庫遭竊——涵蓋超過 1,500 個英國慈善團體——追溯到一把 AWS 存取金鑰:那把金鑰是被它自己的建置流程烤進一個公開的 JavaScript 檔案裡的。沒有人闖進開發者的機器。一個建置工具把環境變數複製進產出物,而那正是建置工具存在的目的。Bromure Agentic Coding 的答案是:那個變數裡沒有任何值得複製的東西。
這個故事的前半段沒有攻擊者。一個建置流程把祕密從環境變數複製進一個 JavaScript 檔案,而一台網頁伺服器把那個檔案交給了每一個開口要它的人。竊取真的發生時,只是 一個 GET 請求。
Beacon 是英國慈善團體用來管理捐款人、支持者與志工的 CRM。8 月 12 日,其技術長 David Simpson 針對公司在 8 月 4 日首次揭露的一起資料外洩,發布了擴充版的事件報 告。The Register 隔天報導了此事, SecurityWeek 在 8 月 14 日跟進, 而根本原因只有一行:
一把可能暴露在公開 JavaScript 建置產出物中的 AWS 存取金鑰。
7 月 27 日 01:20:16 UTC,有人開始使用那把金鑰,並持有存取權一小時二十七分鐘。 Beacon 對於流出內容的評估是:
一份包含 Beacon 所有客戶資料的資料庫副本,連同附件檔案,已被製作,並極可能已 由威脅行為者以可讀格式下載。
這涵蓋超過 1,500 個團體:支持者姓名、電話號碼、電子郵件地址、通訊地址、捐款紀錄 與附件檔案。沒有卡片或銀行資料,因為 Beacon 的客戶並不把那些存在裡面。 ICO 檢視了至少一個受害團體, 認定該團體對這次外洩不負任何責任。這個結論是正確的,同時對一個必須向支持者解釋住 家地址跑去哪裡的募款團隊來說,也是一種冰冷的安慰。
Beacon 對靜態資料做了加密。那什麼也沒改變。 Cybersecurity News 點出, AWS 會代表任何持有有效憑證的人解密。靜態加密保護你的是有人把硬碟搬走的情況,它對 一個握有金鑰的呼叫者沒有任何意見。
值得盯著看的那一段
帶著找出入侵的心情去讀這份事件報告,你不會找到入侵。沒有釣魚郵件,沒有被入侵的維
護者,沒有被下毒的相依套件或提示詞注入,工程師的筆電上也沒有惡意軟體。這個故事裡
沒有任何東西繞過了控制,因為根本沒有人從任何地方把憑證抽出來。Beacon 的建置流程把
一個值從環境變數複製進打包產物,Beacon 把那份產物當成靜態資產部署,而網頁伺服器照
著設計,把它交給每一個提出請求的用戶端。外洩通道是一個 <script> 標籤。
這個機制一點也不奇特。前端建置是刻意把環境變數內嵌進去的,因為瀏覽器在執行期根本 沒有環境可讀:
- Vite 會在建置期把任何以
VITE_為前綴的變數代入import.meta.env。Next.js 對NEXT_PUBLIC_做同樣的事,Create React App 則用REACT_APP_。前綴就是你 用來要求這項代換的方式。 - webpack 的
DefinePlugin與 esbuild 的--define會把你原始碼裡的一個符號換 成字串。它們沒有「哪些字串是祕密」的概念,也無從取得這種概念。 - source map 是第二份副本。伺服器端渲染的框架再加上第三條路徑:一個在程式碼中被 讀取、最後落進客戶端元件的值,會被序列化進傳輸內容裡。
這些副本每一份都來自那個啟動建置的人——或那個東西——的環境。環境是原料,而建置是一 台把原料抄進一個你隨後會發布的檔案的機器。
這一切開始的那台工作站
Beacon 的外洩,就後果而言是一個雲端的故事。它的成因坐落在一台開發者工作站上,而工 作站正在改變。
一個處理工單的代理程式,在一個平常的下午就能做完這一切。它加上一個環境變數,好讓
某個功能能連到某個服務。它編輯 vite.config.ts 或 next.config.js。它寫下讀取
那個變數的那一行,並挑選那一行所在的模組——而這個挑選,正是決定那個值留在伺服器端
還是跨進打包產物的判斷。它執行 npm run build。當部署目標希望產出物被納入版控
時,它就把 dist/ 提交上去。
這裡沒有一件事需要代理程式犯錯,而就算它犯了錯,你也得不到任何訊號。建置會成功, 因為字串就只是字串。打包產物以壓縮成一行的形式出貨,所以差異裡沒有任何東西會刺到 你的眼睛。那個值可以用,所以功能可以動,工單就關閉了。
與此同時,代理程式執行所在的工作空間裡塞滿了憑證,因為憑證正是讓它有用的東西:雲 端金鑰讓它能檢查儲存桶,GitHub 權杖讓它能推送分支,還有登錄檔與資料庫的憑證、一把 模型 API 金鑰。這些每一樣都是一串放在環境裡的字串,而建置流程會把那個環境整份讀 完。
Beacon 的報告對這個安排提出一個直接的問題。你的工作空間裡的某個東西,遲早會把你的 環境複製進一個檔案。問問你自己,它會拿到什麼。
在 Bromure 的工作空間裡,它拿到一個區域字串
Bromure Agentic Coding 讓代理程式跑在 Apple Virtualization 框架上的一台用完即丟的 Ubuntu VM 中,並以一個主機端的 MITM 代理作為它通往網路的唯一路徑。 憑證設計由此推導而出:真正的祕密 留在你的 Mac 上,VM 拿到的是看起來正確、實際一文不值的值。
AWS 有自己的處理方式,因為 SigV4 從不傳送祕密本身。SDK 在用戶端消耗祕密以計算 HMAC,放上線路的是簽章,因此 Bromure 無法像處理 bearer 權杖那樣,在傳輸途中把假 值換成真值。它改為移動簽章的位置。
先從一個建置流程會找到什麼開始。Bromure 匯出到 VM 裡的是 AWS_DEFAULT_REGION 與
AWS_REGION,沒有任何金鑰、祕密或工作階段權杖。理由就寫在原始碼裡,在
SessionDisk.swift 中:
我們在這裡「絕對不可」匯出
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKEN:在 SDK 的鏈中,環境變數會勝過credential_process,而且 它們也會摧毀「磁碟上不存在祕密」這項保證(環境變數會經由/proc、ps -E與 shell 歷史外洩)。
這份外洩管道清單描述的是行程檢視。一個讀取 process.env 的打包工具也屬於這份清
單,而同一個決定就把它涵蓋了。環境中根本不存在 AWS_SECRET_ACCESS_KEY 可供建置內
嵌,所以 DefinePlugin 什麼也沒代換,import.meta.env 什麼也沒帶,而
NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY——刻意犯下的 Beacon 式錯誤——發布出去的是一個
空字串。
SDK 仍然可以運作。~/.aws/config 指向一個 credential_process 輔助程式:
[default]
credential_process = /mnt/bromure-meta/bromure-aws-creds.py
region = eu-west-2
該輔助程式從一個 socket 讀取一份 JSON 文件。它回傳真正的 AccessKeyId——那是用來
識別你、而非驗證你的東西——並搭配一把 Bromure 為此工作階段產生的
SecretAccessKey:四十個字元,取自真正 AWS 祕密所使用的字元集,因此 boto3、
aws CLI 與 Terraform 都會接受它,並照常簽章。而它們產生的簽章,注定會失敗。
接著由主機把它修好。AWSResigner 會辨識任何經由代理送出的 *.amazonaws.com 請
求,剝除來賓端的 Authorization 標頭,並以只存在於主機行程位址空間中的憑證重新計
算 SigV4;當設定檔帶有 STS 材料時,還會加上真正的 X-Amz-Security-Token。你的
terraform apply 可以運作,而祕密從未進入執行它的那台機器。
繞過代理,AWS 會這樣回答:
An error occurred (InvalidSignatureException) when calling the
ListBuckets operation: The request signature we calculated does not
match the signature you provided.
憑證是往關閉的方向失效。一把只有經由你的 Mac 繞行才能運作的憑證,無法在陌生人的筆 電上被行使——而這正是 Beacon 那把被公開的金鑰所欠缺的性質。
盤面上其餘的全是誘餌
AWS 是特例。至於其餘的部分,由線上邊界來完成工作:你設定的每一份憑證,在 VM 裡都 以保留結構的假值現身,由真值加上每次安裝專屬的 32 位元組鹽,經 HKDF-SHA256 推導而 來。真值加密後留在你的 Mac 上,代理則在位元組已經離開 VM 之後,才把真值換上請求, 而且只在請求的目的地正是那份憑證被鑄造給的主機時才這麼做。
這讓「某個工具把環境複製到它不該去的地方」這一整類問題,都以同樣的方式解決,不管 那個工具是什麼:
模型與廠商 API 金鑰
ANTHROPIC_API_KEY 是 sk-ant-api03-brm-…。OPENAI_API_KEY 是
sk-brm-…,XAI_API_KEY 是 xai-brm-…。保留結構,所以各家 CLI 都會毫無怨
言地接受,而在其他任何地方都是惰性的。
Git、登錄檔與雲端權杖
GH_TOKEN 是 ghp_ 加 36 個字元,GitLab 是 glpat- 加 20 個,DigitalOcean
是 dop_v1_ 加十六進位。~/.git-credentials、~/.docker/config.json、
~/.kube/config 與 ~/.config/doctl/config.yaml 全都存在、全都有內容、也全
都是假的。
一個 NEXT_PUBLIC_ 前綴
在任何一個前面加上它,打包工具就會照你說的做。發布出去的產出物帶的是
brm-docker-… 或 ghp_ 形狀的填充物,而這個錯誤讓你付出的是一次重新部署,
不是一份外洩通知。
被提交的 dist/ 或 source map
答案一樣,而且不必仰賴任何人注意到。那份產出物可以躺在公開儲存庫裡、被索引、 被所有還在跑的祕密爬蟲刮走,而它們蒐集到的字串什麼也驗證不了。
Bromure 每次都以同樣方式推導出假值,因此一個會對自己的金鑰取指紋的工具——像 Claude Code 在快取金鑰雜湊時所做的那樣——永遠不會看到憑證在工作階段之間改變。這裡也沒有可 供遺忘的開關。代理是 VM 唯一的對外路徑,所以繞過它的請求帶著的是佔位值,並會在上 游失敗。
被公開的誘餌是一條絆線
Beacon 的金鑰在 7 月 27 日之前,已在一個公開檔案裡待了不知多久,而任何人得到的第一 個訊號,是事後才被讀到的 7 月 27 日與 28 日的 AWS Cost and Usage 報表 出現尖峰。
Bromure 的入侵偵測看守的是 你的 憑證,而不是目的地的信譽,這正是它能對一個從沒 有人點過名的主機發動的原因。一部由工作空間自己鑄造的假值所建成的 Aho-Corasick 自 動機,會掃過每一個外送請求的標頭與內容。一個假值前往它被鑄造範圍之外的主機,就算 作嘗試外洩,而 Bromure 的回應是:
- 代理以 HTTP 451 拒絕該請求,一個位元組也不會抵達目的地。
- Bromure 當場暫停該 VM。
- Bromure 發出警示,點名該憑證、它被鑄造給的主機,以及觀察到它正前往的主機。
Bromure 接著把工作空間標記為已遭入侵,因此下一次啟動需要抹除 VM 的磁碟映像與持久家 目錄。你的權杖、SSH 金鑰與設定會存活下來。
把這個機制對準 Beacon 的情境。一個為某個目的地鑄造的假值,出現在往另一個目的地的請 求裡,這就是特徵;而那個假值是怎麼跑出去的並不重要:外流的打包產物、被偷的 dotfile,或一個帶著瀏覽器、好奇心重的陌生人。第一個試用他撿到的憑證的人,會在他試 用的那一刻,在一份屬於你的紀錄裡自報家門。
Beacon 說它永遠不會有的那份紀錄
Simpson 以 Beacon 所能確立事實的界限,作為事件報告的收尾:
關於這起事件,有些事我們或許永遠無法查明。
Beacon 把這條界限說得很直白。具體是哪些物件、下載的確切目的地,以及哪些物件被存取 過的確定歸屬,都無法從現有的日誌中判定。Beacon 認為整個資料庫都流出去了,這是 從一份帳單報表的形狀推論出來的:Cost and Usage 資料裡的一個傳輸量,與 Beacon 所儲 存內容的概略大小相符。那是在無米可炊的情況下做出的優秀鑑識工作,而現在每一個正在 寫信給支持者的團體,都倚賴著它。
一個 Bromure 工作空間,把那份紀錄當成它運作方式的副產品生產出來。每一個請求都穿過 主機代理,所以代理會把發生的事寫下來:
$ bromure-cli trace ls
HOST METHOD STATUS MS FLAGS
api.anthropic.com POST 200 412 swap×1
s3.eu-west-2.amazonaws.com GET 200 88
registry.npmjs.org GET 200 31
api.github.com POST 201 140 swap×1
trace hostnames 列出一個工作階段接觸過的每一個不同主機,trace summary 把整批
彙總,trace leaks 顯示未受管理的憑證。Trace Inspector(⇧⌘I)提供同樣的視野,還
帶上請求內容,而安全紀錄(Window → Supply Chain Log…)會在供應鏈與 451 的判斷
發生的當下逐筆追蹤。Bromure 把這一切都以保險庫主金鑰加密,靜態保存在你的 Mac 上。
那些 Beacon 只能靠推論回答的問題,在你這裡是一次你自己執行的查詢:這個工作空間跟什 麼說過話、什麼時候、帶著什麼,以及有沒有任何長得像憑證的東西離開過。針對你自己的 資料,在你第一次起疑的那一分鐘裡。
把縫隙關上的那個安排
Beacon 的工程師沒做任何不尋常的事。把憑證放進環境變數是被推薦的做法,而在建置期間 讀取環境變數就是建置在做的事。一整份客戶資料庫,從這兩件合理的事之間的縫隙裡穿了 過去,而再多的小心也關不上它,因為小心是對注意力的一種盼望,不是一項控制。
那項控制,是把事情安排成讓副本一文不值。把真正的憑證留在主機上,交給工作空間一個 能滿足每一個讀取它的工具的佔位值,在執行程式碼的那台機器之外進行簽章與替換,並且 自己保有每一個離開的請求的紀錄。這樣一來,一個盡責工作的建置工具、一個做出看起來 合理的修改的代理程式,以及一個從容讀著你打包產物的陌生人,最後都會抵達同一個地 方:一串在你的 Mac 之外毫無意義的字串。
來源:The Register,「AWS key exposed in JavaScript may have lit way to Beacon's charity data」(2026 年 8 月 13 日) · SecurityWeek,「Over 1,000 Charities Hit by Beacon CRM Data Breach」(2026 年 8 月 14 日) · Infosecurity Magazine,「Exposed AWS Access Key Linked to Data Breach Affecting 1500+ UK Charities」 · Cybersecurity News,「Beacon CRM Confirms Full Database Theft After AWS Access Key Breach」