Pillar Security 在 8 月 3 日公開了一份研究,內容是 Google adk-python 儲存庫裡的兩條攻擊路徑。兩條的結構相同:一個公開面向的 AI agent,能被外部文字操縱去產生足以觸發更高權限 workflow 的留言。Google 在 7 月 21 日確認問題已修復,issue-analyze.yml、issue-fix.yml、pr-analyze.yml 都已從儲存庫移除。
先把界線劃清楚,因為這件事很容易被寫成比實際更聳動的版本:這是研究者在協調揭露下完成的概念驗證,沒有證據顯示真的有人拿它攻擊過 Google,也沒有惡意程式碼因此進到 ADK 的發行版本。 以下若沒有特別說明,研究者的操作都是在他們重建的 PoC 環境裡完成的。
兩條路徑先各給一行:
- 鏈一(信任偽造):PR 描述裡的注入 →
pr-triage.yml的 agent 以adk-bot身分貼出@gemini-cli留言 →gemini-dispatch.yml路由 →gemini-invoke.yml帶著攻擊者的提示執行。 - 鏈二(RCE 與憑證外洩):issue 裡的注入 →
issue-analyze.yml的 agent 貼出/adk-issue-fix→issue-fix.yml的特權 job 開跑 → runner 上任意程式碼執行。
我認為這是今年最值得工程師讀完的一份 CI 安全報告,理由跟 AI 沒什麼關係——它示範了一段條件式明明正確實作了既定規則,卻因為規則本身信任了一個可被操縱的身分,整道防線就失去意義。
閘門沒有被繞過,它是照規則放行的
先看鏈二的授權閘門,因為它是整件事最反直覺的部分。
issue-fix.yml 有權限檢查,而且寫得比多數人的 workflow 嚴謹。下面只節錄並等價改寫它的觸發條件,方便閱讀——這不是逐字原始碼,也不是可直接執行的完整 workflow,runs-on、permissions、steps 與部分 workflow_dispatch metadata 都省略了。逐字版本可以在 v2.2.0 的歷史檔案裡核對:
1 | on: |
它鎖了儲存庫名稱、排除 PR 只認 issue、要求指令出現在留言開頭、還檢查發話者跟儲存庫的關係。以人類為對象的威脅模型來看,這是一道合格的閘門。
而 adk-bot 的身分是 COLLABORATOR,本來就在白名單上。它不是被繞過的,它是被正常放行的——這個區別很重要,因為沒有任何一行條件式寫錯,你去 code review 也挑不出東西來。
閘門回答的是「你是誰」,它從來沒被要求回答「你這次說的話是誰教的」。在人類的世界裡這兩件事幾乎等價,帳號被盜是例外而不是常態。agent 讓它們分開了。
鏈一:偽造一份「已經有人審過」的軌跡
先講影響比較間接的那條。
pr-triage.yml 會對進來的 PR 跑自動分析。研究者在 PoC 環境裡送出一個 PR,把提示注入寫進 PR 描述,偽裝成符合貢獻規範的 triage 步驟。agent 讀進去、照做,以 adk-bot 的身分貼出一則 @gemini-cli 開頭的留言。
這則留言被 gemini-dispatch.yml 接走,路由到原本只允許受信任儲存庫關係觸發的 gemini-invoke.yml。高權限的 agent 就這樣帶著攻擊者寫的提示跑起來,並且在回應裡吐出了自己掛載的 MCP 工具清單。研究者另外證明 github-actions[bot] 可以送出 pull request review 事件、核准變更。
研究者在 PoC 環境裡示範了怎麼製造出一份假的審查軌跡,也錄了影片。但他們沒有在 Google 的正式儲存庫完成惡意合併——最後那一步(讓 maintainer 真的按下 merge)是推導出來的社交工程風險。Google 的漏洞獎勵計畫回覆針對這條鏈指出,PR 不會因為 bot 審過就自動合併。
我覺得這條鏈真正棘手的地方在於——它攻擊的不是機器,是人類 reviewer 賴以判斷的那些訊號。 你看到 PR 上有 approve、有 bot 分析留言、狀態一片綠,這些平常拿來省時間的證據,此時全都在攻擊者的影響範圍內。
鏈二:從一則 issue 到 runner 上的 RCE
這條比較嚴重,也是研究者在調查過程中額外挖到的。
issue-analyze.yml 對新開的 issue 沒有任何作者身分檢查,任何 GitHub 使用者開 issue 都能觸發。它會執行 Antigravity agent,過程中 ADK_TRIAGE_AGENT(adk-bot 的 personal access token)和 GOOGLE_API_KEY 是直接以環境變數帶入的;ADK_GCP_SA_KEY 則是先餵給 Google 的 auth action 當 credentials_json,由它產生憑證檔落在 runner 上。分析結果一樣以 adk-bot 的身分貼成留言。
注入寫在 issue 裡,agent 讀了,貼出 /adk-issue-fix。前面那道閘門看到發話者是 COLLABORATOR,照規則放行,特權 job 開跑。
跑起來之後還有一層防護:Pillar 指出 run_antigravity.py 會先擋掉一組危險字元,再用 shlex.split() 取第一個 token,只准 gh 和 git。聽起來很嚴,但它只檢查了指令名稱——後面的子命令和參數完全沒驗,而且 agent 的檔案寫入工具根本不在這個檢查的管轄範圍內。
於是 git 自己就是逃逸出口:
1 | # 注意:以下 payload_dir / 指令內容都要換成實際值。 |
這兩條的第一個 token 都是 git,也都避開了那組被封鎖的字元。搭配沒被管住的檔案寫入工具,攻擊者可以先把 payload 寫進 runner,再用合法的 git 指令執行它。
順帶說明一個容易誤解的地方:不是「任何 git 動作都會觸發 hook」。git status、git log 這類不會。要嘛執行到 hook 生命週期上的動作(例如 commit),要嘛像上面那樣直接用 git hook run 叫它。
PoC 影片展示的結果是把 adk-bot 的 PAT 送到研究者自架的 C2 伺服器。同一個執行環境裡還有 GOOGLE_API_KEY 和 auth action 產生的 GCP 憑證檔,RCE 拿得到——但「用這些憑證去推程式碼」或「橫向打進 GCP 專案」屬於報告列出的潛在影響,不是已示範的成果。
那到底該怎麼擋
既然問題是「身分不等於意圖」,直覺的反應是把身分檢查寫得更細。這條路走不通,而且有個很多人會踩的陷阱:
1 | # ❌ 看起來合理,實際上擋不住這次的攻擊 |
只有被 GitHub API 標示為 Bot 類型的 actor 才會命中這個條件。像 adk-bot 這種綁 personal access token 的機器帳號,即使完全由自動化控制,類型仍然是 User——它會直接通過。更麻煩的是 GitHub App 還能以「代表某位使用者」的方式發言,那時候動作會歸屬到那個真人頭上。想用 user.type 證明「這段文字是真人親手打的」,從一開始就不成立。
author_association 收緊到 OWNER/MEMBER 確實能擋掉這次的 adk-bot。但我只把它當止血,不當解法:它成立的前提是 agent 的權限層級剛好低於你設的門檻,哪天有人把 bot 升成 MEMBER 就破功;而且 MEMBER 只代表組織成員,不代表這個人是這個儲存庫的 maintainer,大型組織裡這個範圍可能寬得離譜。
真正的分界不在條件式,在於別讓吃外部輸入的 job 和握有 secrets 的 job 共用同一個身分。條件判斷是在猜「這次是不是壞人」,身分隔離是讓壞人猜對了也拿不到東西。要人類把關就用 GitHub 的 environment approval,並且確保 reviewer 身分與憑證不會落到 agent 手上——重點是建立一道 agent 靠留言滿足不了的核准邊界。
三件現在可以做的檢查
不需要有 AI,傳統的 bot 一樣適用。
把吃外部輸入的 workflow 標出來。 觸發條件是 issues、issue_comment、pull_request_target 的都算。pull_request_target 值得單獨提:它在 base 儲存庫的特權 context 下執行,預設 checkout 的是 base branch 而不是 PR 的程式碼,真正的危險是 workflow 自己額外指定 PR head 去 checkout 並執行,或者把 PR 標題、內文、branch 名稱這些沒轉義的字串直接塞進 shell。
追一次傳遞性權限。 別只盤點「哪個 workflow 有 secrets」,要盤點「低權限 job 的輸出,會不會被高權限 job 當成命令或授權訊號讀進去」。留言、label、branch 名稱、commit message 都是 job 之間傳話的管道。單純被讀到不構成漏洞,構成漏洞的是下游把它當成可信的指令來源。
別讓 agent 用人類帳號的長期 token。 GitHub App 的 installation token 比 PAT 好,短期、可限 scope、可限儲存庫;GCP 那邊用 OIDC 換掉長期有效的 service account JSON key。要注意這只縮短憑證的有效期,runner 當下被控制時,攻擊者還是能立刻拿現成的憑證去用——它降低的是事後的損害範圍,不是當下的。
我的判斷
自然語言進入了授權路徑,這是我覺得這起事件真正要記下來的一句話。
過去十年 CI 的權限模型是為人類設計的:你是不是 collaborator、有沒有 write 權限、這個 token 的 scope 多大。這套模型的前提是「帳號的行為代表帳號持有者的意圖」。agent 打破的正是這個前提——它有帳號、有 token、有權限,而它的行為可能被最後讀進去的那段外部文字主導。
我最在意的不是某個工程師漏了一行檢查,而是這次根本沒有人寫錯。那道閘門在它被設計的年代完全合理,然後前提悄悄變了,而條件式不會自己知道這件事。你的儲存庫裡如果有掛著 write 權限、又會讀外部文字的自動化,那就已經具備這類攻擊鏈的兩個關鍵前提,值得再往下查一層:那些外部文字能不能影響它的工具呼叫,它的輸出又會不會被下游當成授權訊號。
Google 的處理是把那幾個 workflow 直接移除。看起來粗暴,我覺得誠實。
延伸閱讀:AI Agent 拿到的權限比實習生還大講的是同一個問題的框架面,Prompt Injection 不只是學術問題則是這類攻擊的背景。
參考資料
- I'll Just Call You: Agent-to-Agent Privilege Boundary Failures in CI/CD on Google's ADK Repository — Pillar Security
- issue-fix.yml @ v2.2.0(本文節錄的原始 workflow)
- Google Deletes 3 ADK AI Workflows After Malicious GitHub Issue Could Trigger Privileged Agent — The Hacker News
- Preventing pwn requests — GitHub Security Lab









