它的任務是一則 issue,它的權杖卻讀得到整個組織
Noma Security 的 GitLost 展示了一個未經驗證的陌生人在公開的 GitHub 儲存庫開一則 issue,就讓 GitHub 的 AI 代理把一個私有儲存庫的內容以公開留言貼了回來。提示詞注入上了標題,但代理的權杖——範圍涵蓋整個組織,而不是它正在分流的那一則 issue——決定了這次洩漏能走多遠。任務所需與環境所持之間的那道落差,才是值得帶回家的部分,也是當你自己託管代理時,可以設計得不一樣的部分。
一個未經驗證的陌生人在一個公開的 GitHub 儲存庫開了一則 issue,寫下一段文字,然後等待。GitHub 的 AI 代理讀了這則 issue,走進了組織的私有儲存庫,把一份私有的 README 以公開 留言貼了回來。那段文字是扳機。而代理的權杖——範圍涵蓋整個 組織,而不是眼前那一則 issue——是這次洩漏走得那麼遠的原因。
Noma Security 的研究團隊在 GitHub 上架起一個組織,方式和如今
許多團隊一樣:幾個公開儲存庫、幾個私有儲存庫,再接上 GitHub
Agentic Workflows 來處理雜務。GitHub Agentic Workflows 是純
Markdown 的指示,由 GitHub Actions 交給一個以 Claude 或
Copilot 為後盾的 AI 代理,讓代理能代表團隊分流 issue、回答
問題、開 pull request。接著研究人員扮起攻擊者。他們在其中一個
公開儲存庫裡開了一則看起來平凡無奇的 issue,在內文裡塞進幾句
多出來的話,讓工作流程跑起來。代理讀了 issue,抓取了一個私有
儲存庫裡 README.md 的內容,把它貼進一則任何人都讀得到的
公開留言。
攻擊者沒有用密碼、沒有偷來的金鑰、沒有漏洞利用程式碼,也沒有 CVE。Noma 把這個手法命名為 GitLost, 回報給 GitHub,並在該公司知情下發表。整個酬載就是一段英文 散文,坐在一個代理生來就該閱讀的欄位裡。
一則用私有儲存庫作答的 issue。
機制很短,這正是它值得畫出來的原因。一個工作流程在某個事件上 觸發,例如一則 issue 被開啟或被指派。代理把這則 issue 的標題 與內文當成它的任務來讀。埋在那段內文裡的,是要代理抓取一個 私有儲存庫的 README 並貼成留言的指示。GitHub 在這前面設了 護欄:沙箱、預設唯讀的權杖、輸入清理,以及對文字的一道威脅 偵測。Noma 嘗試各種變體,直到其中一個溜了過去。在惡意請求 前面加上「Additionally(此外)」這個詞,就足以繞過過濾器, 因為模型把它讀成一項接續的任務,而不是一個該拒絕的請求。 一字之差,就把注入送過了線。
換個措辭就贏過過濾器。
一個讀取 issue 內文、判斷其中是否含有攻擊的過濾器,是在從措辭 猜測意圖,而措辭的變體無窮無盡。Noma 沒有攻破模型,也沒有找到 記憶體臭蟲。他們把同一個請求換句話說,直到分類器給它的分數 落到自己的門檻之下,而「Additionally」正是落地的那個措辭。
這是使用工具的代理的結構性特質,我們上週寫過: 代理把它的指示和它抓來的資料當成同一條扁平的 token 流來讀, 並靠語氣與位置去推斷哪一部分是命令,就像人略讀那樣。issue 內文是資料。注入這一招,就是把不受信任的資料塑造成命令的 樣子,賭模型會照著行動。偵測有幫助、值得跑,但它會漏:分類器 有門檻,而一個有無限次嘗試機會的攻擊者會一直試探,直到某個 措辭的分數落到門檻之下。
決定洩漏大小的那道落差。
GitLost 有一部分能在 GitHub 之後推出的任何過濾器下存活。工作 流程的任務是分流一個公開儲存庫裡的一則 issue。GitHub Actions 放進那個工作流程環境裡的權杖,卻對組織內每一個公開與私有 儲存庫都有讀取權限,因為團隊會授予那樣的廣度,好讓代理能撈取 跨儲存庫的脈絡。攻擊者選了目標;權杖的範圍決定了損害的上限。 把攻擊換上一百種說法,那個上限不變:權杖看得到什麼,就是什麼。
Noma 自己的建議也指向同一道接縫。他們給防禦者的建議是把權杖 限縮到工作流程所分流的那一個儲存庫, 而不是整個組織。這個建議等於承認注入有時會得手,而當它得手 時,波及範圍應該是任務的大小,而不是帳號的大小。
外洩路徑正是代理的日常工作。
那份私有 README 是經由一則公開留言離開組織的,而留言是代理 本來就該做得到的事。在 issue 上留言是它的功能。線路上沒有任何 東西看起來像竊盜:沒有奇怪的端點,沒有從側門溜走的編碼資料塊。 一個緊盯惡意流量的網路監控器,只會看到代理貼出一則留言—— 它整天都在做這件事。當外洩管道是一項被許可的能力時,划算的 防禦是打從一開始就限制代理搆得到什麼。
你有權改變架構的地方。
GitLost 有兩件事在你能力範圍之外。這個臭蟲是 GitHub 的,要由 GitHub 修,而 GitHub 正在修。而且代理跑在 GitHub 的伺服器上、 GitHub Actions 之內,沒有任何你安裝的產品站在那條路徑上。 Bromure Agentic Coding 跑在開發者的 Mac 上。它不站在 GitHub 伺服器端工作流程的前面,也不聲稱如此。如果你對這一類問題的 唯一曝險,是一個由別人維運的託管代理,你手上的槓桿就是 Noma 點名的那一個:把權杖限縮下去。
可帶走的教訓,是給越來越多在自己掌控的機器上跑編碼代理的 團隊的——在那裡,架構由你來定。有兩條原則能從 GitLost 帶 過來,而且都不依賴抓到注入。讓環境保持精簡,被劫持的代理便 沒什麼值得拿的可繼承。把憑證限縮到任務上,一次淪陷的觸及範圍 便是工作的大小,而不是帳號的大小。Bromure Agentic Coding 就是 這些原則的一種安排。
Bromure Agentic Coding 把你的編碼代理——
Claude Code、Codex 或 Grok——跑在一台可拋棄、設定檔專屬的
Linux VM 裡:自己的核心、自己的檔案系統、自己的網路堆疊,在
Apple 的 Virtualization framework 上,與你的 Mac 之間隔著一層
hypervisor。一個設定檔是一個連貫的工作範圍,一個客戶或一項
服務。真正的憑證從不進入那台 VM。Bromure 把它們放在主機上的
一個仲介後面,發給客體機一些在讀取它們的工具眼中足以亂真的
假值;你 Mac 上的一個 proxy 會在請求離開時、在線路上把樁換成
真祕密,而握著金鑰的沙箱
一文走過這套機制。一個被說服對憑證檔案跑 cat 的代理,找到的
是一個占位符,因為它想要的那個值,從來不在邊界屬於它的那一側。
代理真正花得出去的那把權杖是限縮且會過期的,所以一次洩漏是 一場狹窄、有時限的使用,而不是一份永久的授權。整個工作區是 一台在你關閉工作階段時就消失的 VM。把 GitLost 的形狀丟進這套 安排跑一遍,注入仍然可能得手,因為隔離不會把一個被騙的模型 騙回來。一個被說服去外洩的代理,伸手去拿憑證時找到的是樁, 花掉的是一把只涵蓋它當時所在那個設定檔的權杖,而且坐在一個 沒有任何路徑通往你機器其餘部分的盒子裡。淪陷的觸及範圍,就是 任務的大小。
讓環境保持精簡
被劫持的代理只能交出它環境裡有的東西。客體機裡放樁、真 祕密留在主機上,意味著得手的注入在它以為有金鑰的地方, 找到的是占位符。
把憑證限縮到任務上
Noma 給 GitHub 工作流程的建議是同一條原則:一把只涵蓋一個 儲存庫的權杖,就把損害封頂在一個儲存庫。範圍與期限把一次 洩漏變成一場狹窄、有時限的事件。
讓盒子可拋棄
當工作區是一台關閉即蒸發的 VM,持久化便沒有任何耐久的 落腳處,波及範圍也隨著工作階段一起結束。
假設它一定會被注入
偵測值得跑,也一定會漏。把系統設計成漏掉一次也承受得住, 而不是賭自己抓得到每一種措辭。
限縮解決不了的部分。
限縮不會讓代理安全到可以對準一個真祕密。如果你把一把活的憑證 放進 VM,而代理被說服讀了它,沒有任何 hypervisor 能把它「讀 回去」;改變的是代理搆得到什麼,而不是一個被騙的模型會不會 繼續被騙。Noma 點出的外洩問題,也有一塊是光靠隔離關不上的: 一個能留言、能開 PR、能發訊息的代理,可以把資料從那條被許可 的管道搬出去,而限縮決定的是搆得到多少資料可搬,不是那條管道 存不存在。至於 GitLost 本身,它屬於 GitHub。他們修補時記得 更新,在那之前先把你的工作流程權杖限縮下去,因為那是平台交到 你手上的槓桿。
無論你在哪裡跑代理,研究人員都會繼續回報注入,而代理環境裡 那把憑證的範圍,也會繼續決定每一次注入的結局:是一筆被記錄 下來的有驚無險,還是一個私有儲存庫出現在公開頁面上。Noma 的 修法和 Bromure 的架構指向同一個方向:給代理任務所需的觸及 範圍,一寸也不多。
Bromure Agentic Coding 在 Apple Silicon 上以可拋棄的 Linux VM 執行 Claude Code、Codex 與 Grok,真正的憑證留在主機的仲介 後面、在線路上才換入,對代理的輸入做提示詞注入偵測,並把每 一次呼叫寫進一份代理無法編輯的軌跡。它免費、開源,今天就在 bromure.io 出貨。感謝 Noma Security, 他們的 GitLost 報告 促成了這一篇。