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

登入就已經夠了

一個在清磁碟空間的開發者,在一個隱藏目錄裡發現 700 MB,然後把線頭一路拉下去:Z.ai 的 ZCode 一直在把他整個工作樹打成 tar,42,411 個檔案裡有 86.6% 是 .git,用一支只有 Z.ai 握著的金鑰加密,然後直接送進一個 Alibaba Cloud 儲存桶。沒有攻擊者,沒有 CVE,沒有惡意程式。觸發條件就是處於登入狀態。客戶端把封存檔加密了,所以沒有掃描器讀得懂,剩下的事實只有目的地和大小。那正是 Bromure Agentic Coding 為工作區發出的每一個請求記下的欄位中的兩個,而且記在一條 VM 裡的工具沒得選的線路上。

你的編碼工具不需要被入侵,就能把你的原始碼送到別處去。它只需要一個預設值。信不信 任這家廠商不是問題。要問的是:你會發現嗎?

你正在一顆滿了的磁碟上清空間。家目錄裡有個隱藏目錄佔了七百多 MB,而你不記得裝過 任何那種大小的東西。在它裡面,一個叫 checkpoints 的資料夾底下,只有一個 313 MB 的檔案,副檔名是 .enc,旁邊一筆中介資料寫著 failureCount: 564。

五百六十四次嘗試,做的是你從來沒要求過的事。

七百 MB,還有一條可以拉的線

那就是代號 ferstar 的中國開發者在 9 月 18 日開始的地方,而 他對自己發現的東西所做的逆向工程紀錄 值得整篇讀完。那個目錄屬於 ZCode,也就是 GLM 模型背後那家公司 Z.ai 的桌面 AI 編碼客戶端。CSO Online 和 InfoWorld 兩邊都原字不動地引了他對這個行為的總結:

「只要你處於登入狀態,ZCode 就會靜靜地把你整個工作區打包 — 完整的 .git 歷史、 LFS 資產快取、reflog,以及全域應用設定 — 加密,然後直接上傳到 Aliyun OSS。」

觸發條件就在第一個子句裡。不是你問的某個問題,不是你打開的那個索引功能:一個有效的 工作階段權杖,那是擷取程序唯一在等的東西。他的紀錄裡有一個工作階段記下了 62 次擷取 事件。當他把封存檔刪掉,客戶端在半小時內又造了一個新的,並把失敗計數加一。

盒子裡裝了什麼

他拆開的那份快照裡有 42,411 個檔案,加密前 345 MB,加密後 313 MB。這 345 MB 是由什麼組成的:

  • .git/lfs/:196.1 MB,56.8%
  • .git/objects/:102.2 MB,29.6%
  • .git/logs/:0.6 MB,0.2%
  • 其他全部,也就是真正的原始碼與文件:46.2 MB,13.4%

這次上傳有 86.6% 是 .git。 那份封存檔帶走了儲存庫歷史裡的每一個物件、LFS 快取抓下來的每一個大型二進位檔,還有 reflog——它列出你待過的每一個分支,以及你做了 又撤掉的每一個 commit。讀你工作樹的人知道你正在出什麼貨;讀你 .git 的人知道你 試過什麼、你回退了什麼、以及是誰 commit 的。

一個登入權杖、四個步驟、沒有攻擊者你的機器,已登入工作目錄.git/lfs, .git/objects, .git/logs程式碼、docs、全域應用設定42,411 個檔案 · 345 MB設定裡的兩個開關體驗最佳化 → 訓練用途快照索引 → 伺服器端索引兩個都不管打包這件事1 · zcode.z.ai回傳一份 OSS 表單簽章以及一支 RSA 公鑰私鑰那一半留在伺服器2 · 本機打包並封裝tar.gz → AES-256-CTR金鑰 ← RSA-OAEP-SHA256313 MB,你讀不了3 · POST 到 Alibaba Cloud OSSzcode-prod 儲存桶直接 — 不經過 Z.ai 的伺服器每次擷取事件一個物件4 · OSS 回呼告訴後端物件已經落地快照在他們那邊被登記並且可以建索引是什麼把它啟動的一個有效的登入權杖。不是提示詞,不是問題,也不是那個索引功能。同一個工作階段裡 62 次擷取事件。把封存檔刪掉,三十分鐘內就被重建。failureCount: 564
封存檔是怎麼離開這台機器的。ZCode 客戶端向 zcode.z.ai 要上傳憑證,收到一份 OSS 表單簽章以及一支 RSA 公鑰;它把工作目錄打成 tar,用那支公鑰包起來的金鑰在本機以 AES-256-CTR 加密封存檔,然後把結果直接送進 Alibaba Cloud 的物件儲存,繞過 Z.ai 自家的應用伺服器,再由一個 OSS 回呼告訴後端物件已經落地。對應的私鑰從來不會到客戶端手上,所以使用者無法解開自己的封存檔。觸發條件是一個有效的登入權杖,而不是你對助理做了什麼。有一個工作階段記下 62 次擷取事件,而把檔案刪掉會在大約三十分鐘內引發重新打包。

兩個開關管的都是他們那一邊

ZCode 出貨時帶著兩個設定,一個仔細的開發者會找到並關掉它們:一個是體驗最佳化, 一個是儲存庫快照索引。兩個都真的在做某件事,而兩個都沒在做名字暗示的那件事。

第一個管的是 Z.ai 是否被授權使用那些資料。第二個管的是快照送到之後伺服器要不要 建索引。打包和上傳照跑,因為那個擷取的側車只要權杖提供者遞給它一個有效的 JWT 就會啟動,整條路徑上任何地方都沒有使用者偏好。把兩個開關都關掉,客戶端還是把你的 儲存庫打成 tar、還是加密、還是送進儲存桶,並且承諾對之後發生的事保持低調。

這是一家廠商的錯誤,但這個形狀到處都在重演。住在它所限制的那個程式內部的控制項, 只是一個請求。它跑在同一份程式碼上——而那份程式碼若沒出錯,你根本不需要它。

一份快照:42,411 個檔案,加密前 345 MB.git/lfs — 196.1 MB.git/objects — 102.2 MB程式碼 46.2 MB56.8%29.6%13.4%離開這台機器的東西有 86.6% 是儲存庫的歷史 — 物件、LFS 快取、reflog光是 reflog 就列出你待過的每一個分支,以及你做了又撤掉的每一個 commit。全域應用設定檔也一起被帶走了。
345 MB,其中只有 46 MB 是程式碼。ferstar 復原的那份快照裡有 42,411 個檔案:196.1 MB 的 Git LFS 快取、102.2 MB 的 Git 物件、0.6 MB 的 reflog,佔了整份載荷的 86.6%,對上真正的原始碼與文件 46.2 MB。一個會上傳你工作目錄的工具,等於上傳你在那個儲存庫裡寫過的一切,連你放棄的分支都算進去。

線路上沒有任何東西讀得懂它

客戶端在每一份封存檔離開機器之前就用 AES-256-CTR 把它加密,並用 zcode.z.ai 交給 它的一支 RSA-OAEP-SHA256 公鑰把對稱金鑰包起來。對應的私鑰從來不會下來。ferstar 打不開自己的封存檔;唯一打得開的那一方,是握著儲存桶的那一方。

這排除掉你手上所有靠讀位元組運作的防禦。祕密掃描器在那個 tarball 裡找不到任何 AWS 金鑰,而深度封包檢測看到的,是一條通往儲存端點、載著被密文包住的密文的 TLS 工作階段。

活下來的事實有兩個:它去了哪裡,以及它有多大。一個 313 MB 的 POST 送往你的 團隊根本不部署的區域裡的物件儲存主機,是會顯眼的——前提是有人把你的工具連去哪裡、 送了多少寫下來。沒有人寫,所以找出這件事靠的是磁碟空間考古學和一個星期的逆向工程, 而不是瞄一下紀錄。

出口在哪裡

Bromure Agentic Coding 把代理和它的工具鏈放進你 Mac 上的一個 Ubuntu VM 裡,並且 只給那個 VM 一條通往網際網路的路:一個跑在主機上、在客體之外的 man-in-the-middle 代理,位置在 VM 裡任何程序都無法重新設定的地方。

這對軟體沒有任何要求。虛擬網路交換器會把 VM 的 80 埠與 443 埠流量透明地導進主機 的代理,不需要任何環境變數,也沒有任何客體能取消的東西,而且在每個工作區都預設 開啟。一個忽略 HTTPS_PROXY、自己開 socket、直接跟儲存端點講話的工具,還是會經過 它,因為攔截住在客體插進去的那台交換器裡,而不是住在客體裡。

接著,一個沒人要求的 313 MB POST 會遇到四件事。

每個請求都留下紀錄

每個經過代理的請求都留下一筆追蹤紀錄:時間戳、主機、埠、方法、路徑、狀態、 延遲,以及在任何憑證替換之前量到的請求位元組數。你不需要解開一份封存檔, 就能注意到有個儲存庫大小的上傳送往一台你從來沒部署過的主機:大小和目的地都是 欄位。用一個排序,用另一個篩選。

它根本沒離開 Mac

每個工作區各自的出向防火牆是一張有序規則表(動作、協定、主機或 CIDR、埠、 HTTP 方法),結尾是 default allow 或 default deny。用白名單的方式跑一個 工作區,那份上傳就無處可去。有兩個地方在執行規則:交換器比對目的地 IP 和從 DNS 偷聽到的主機名,代理再比對一次 TLS 伺服器名稱,而你的修改會立刻送到正在 跑的工作階段。

每個判決都是一列

每一個防火牆決定都落進 Security Timeline,所以一個一直重試的工具——比方說 564 次——會以對某個具名主機的重複拒絕出現,而不是以一顆慢慢被填滿的磁碟出現。 那個花了一星期才重建出來的樣式,正是那個視窗生來要顯示的東西。

它打包的那些設定都是誘餌

ferstar 那句話裡最該讓你擔心的詞是「全域應用設定」。在一個 Bromure 工作區裡, ~/.git-credentials、~/.docker/config.json、~/.kube/config 和 ~/.aws/config 都存在也都能用,而它們裡面的每一個值都是保留結構的假貨。VM 裡沒有任何檔案、環境變數或程序,會握著真的 API 金鑰、OAuth 權杖、AWS 祕密或 SSH 私鑰。

剩下的由範圍處理。一個工作區就是一個專案,或者一道憑證邊界;兩個工作區絕不共用一個 VM,而客體的家目錄是它自己的映像檔。一個工具在工作區裡打進 tar 的東西就是那個 工作區,這就把你 Mac 上另外四十個儲存庫、你主機的 ~/.aws/config、還有你的 SSH 金鑰留在外面。那些金鑰根本不會進到客體:簽章是在主機上透過一道 vsock 橋接完成的, 私密的位元組留在那裡。

Mac 上的那個工具家目錄~/work/* : 每個儲存庫、每個 .git~/.aws/config, ~/.docker, ~/.ssh : 真的工具自己選擇讀什麼、送到哪裡。工具自己執行自己的設定。313 MB,已加密,送往一個儲存桶沒有目的地的紀錄。沒有大小的紀錄。你怎麼發現的磁碟滿了,於是你開始做逆向工程。工作區裡的那個工具工作區 VM — Ubuntu 客體,一個專案/home/ubuntu — 這個儲存庫、這份簽出~/.aws/config, ~/.docker — 佔位值工具還是自己選擇讀什麼、送什麼。路線不是它選的。:80 與 :443 被導向主機端代理主機 · 方法 · 路徑 · 請求位元組 → tracedeny web *.aliyuncs.com · default deny在線路上就被拒絕,並寫進 Security Timeline。
同一個工具,兩種擺法。直接擺在 Mac 上跑,它就坐在你擁有的每一個儲存庫、家目錄裡每一個憑證檔案,以及一條沒有中介的網際網路路線旁邊;上傳就是一條和其他一樣的 TLS 工作階段,而封存檔已經加密,所以任何讀內容的東西都幫不上忙。擺進一個 Bromure 工作區裡跑,它的 80 埠與 443 埠流量會被導進一個它沒辦法重新設定的主機端代理:代理記下目的地和位元組數,出向規則決定這個請求到底走不走得出去,而它打包的那些設定檔裡裝的是佔位值,真正的值從來沒進過那個 VM。

Z.ai 接下來做了什麼

Z.ai 在 ferstar 發表的當天就道歉了。公司在 ZCode v3.14.0 裡關掉了那條上傳 流程;InfoWorld 報導這個版本在 9 月 19 日出貨,更新項目裡列了一條修正儲存庫上傳 異常的問題。公司並請來安全公司 NSFOCUS 稽核結果。NSFOCUS 確認「zcode-prod 這個 Alibaba Cloud OSS 儲存桶裡的所有資料物件,以及儲存桶本身,都已刪除」,而 Z.ai 說 稽核之後「沒有識別出任何能夠觸發產生本機儲存庫快照,或把本機檔案往外傳送的功能 路徑」,並且那份程式碼「從未被用於模型訓練」。接著他們把整套開源了:客戶端、後端、 共用 UI、Agent CLI 與執行環境,採 Apache-2.0,放在 GitHub 上。這高於業界常態,也意味著下一個想查的人不 需要反組譯器。

Semgrep 的 Cris Thomas 在 CSO 的報導裡被引用時,替這個類別命了名:「這其實不是 AI 模型的問題,而是一個老派的安全架構問題。」他希望廠商公開什麼東西離開了機器、 去了哪裡、保留多久,而且「預設打開的是最小權限,不是最大權限」。這件事沒有客戶能 從外面強制。

預設值就是威脅模型

這裡沒有人攻擊任何人。沒有人偷憑證、沒有人汙染套件、沒有人把模型說服成什麼。一個 產品做了它的作者把它造出來要做的事,而他們把它造出來要做的,是把你的儲存庫送到一個 你讀不到的地方。這起事件是一個預設值,而挑選預設值的,是利益跟你的只是相鄰的人。

這就是把邊界放到工具之外的理由。你不必相信你的編碼助理是敵人;你只需要注意到,看 不到的東西你沒辦法稽核,而廠商自己的設定面板是個放那個開關的怪地方。讓代理和它的 工具鏈跑在這樣的地方:它們開的每一條連線都要穿過一條你擁有的線路,那次穿越的紀錄 寫在你的 Mac 上,而那條擋下 313 MB 上傳的規則,是你自己打進去的一行,不是別人替你 尊重的一個勾選框。

那麼「我的編碼工具在送什麼?」就變成星期二下午打開的一個視窗,而不是一個研究專案。