代理的令牌是组织所有者的
10 月 23 日,LiteLLM 公布了遭投毒的 1.107.0 版本的根本原因。第一个被盗的凭据并非来自日志,而是来自 LiteLLM 自己的代理平台:它的 API 会返回云端代理用来开 pull request 的 GitHub 令牌,而那是组织所有者的令牌,权限没有收窄。这个 API 的密钥在一份公开配置文件里放了四个半月。在 Bromure Agentic Coding 中,开 pull request 的代理拿到的是替身,真正的令牌加密保存在 Mac 上,没有任何 API 会返回它。
LiteLLM 的代理需要一个 GitHub 令牌来开 pull request。它们拿到的,是属于组织所有者的令牌。一次使用泄露密钥的 API 调用,就把它取了回来。
10 月 23 日,LiteLLM 公布了其 1.107.0 版本背后的攻击链,也就是 10 月 21 日夹带凭据窃密木马发布的那个版本。这篇文章自称“权威的唯一事实来源”,并修正了 LiteLLM 最初的推断。被盗的 GitHub 令牌并非来自记录下来的模型流量。第一个令牌来自 LiteLLM 为自家编程代理运行的一项实验性服务。
我们在那个版本才发布两小时中报道过这次发布本身,当时沿用的是最初的推断。最终报告的起点不同:攻击者拿到的第一个凭据,属于那些代理。
握有所有者令牌的代理平台
用 LiteLLM 的话说,LiteLLM Agent Platform 是“一个让自托管云端代理向 GitHub 仓库提交 PR 的实验性项目”。为了开 pull request,平台持有一个 GitHub 令牌。这个令牌“没有关联服务账号,权限也没有收窄”。它是组织两位所有者之一的 GitHub CLI OAuth 令牌。
报告中的时间线(UTC):
- 5 月 27 日 18:53。 一个
opencode.json文件被提交到公开仓库 LiteLLM-Labs/lite-harness。那是一个代理运行框架的配置文件,里面有 Agent Platform 某个部署的主密钥。团队发现后,用 force-push 覆盖了那次提交,并把暴露的密钥标记为待吊销。“主密钥的吊销并未执行。” - 10 月 10 日 21:27。 攻击者从 69.48.229.143 用这把密钥调用平台的 API。
GET /api/v1/managed_agents/agents?limit=100返回了每个受管代理的配置,连同所有者的令牌。 - 10 月 10 日 22:11。 攻击者用所有者的令牌,把一个一次性分支
ci-tmp-a351cc推送到 BerriAI/litellm。它的工作流把六个仓库机密发到 webhook.site。其中的GH_TOKEN,是第二位维护者的 GitHub CLI 令牌。二十四秒后,BerriAI/litellm-auto 里的第二个分支把第三位维护者的令牌发到同一个端点。两次运行都在三分钟内被删除。 - 10 月 12 日。 私有仓库上又有其他工作流,把能接触到的每个仓库机密都发了出去。
- 10 月 21 日 10:08。 用第二位维护者令牌推送到发布仓库的变更,被分支保护拦下。五秒后,所有者的令牌关闭保护并 force-push 这项变更,让发布工作流指向仿冒的 Berri-AI/litellm。
- 10 月 21 日 11:11。 第二位维护者的令牌批准了生产环境关卡。原本有条规则要求由绑定在 LiteLLM 办公室某台设备上的账号批准,那一周被放宽了,于是任意两名工程师都能发布。
- 10 月 21 日 11:39 至 11:46。 七个 wheel 上了 PyPI,接着是 Docker Hub 和 GHCR 的镜像。
- 10 月 21 日 11:53。 第三位维护者的令牌删除了发布运行的记录。12:24,所有者的令牌用 force-push 把 main 恢复干净。
LiteLLM 自己的总结是:“这起事件有力地说明了,不轮换可能已暴露的密钥会带来什么风险。”该公司现在正在吊销与每个内部系统、每个供应商平台相关的所有令牌。
每支代理队伍都握有这样的令牌
LiteLLM 有一支能干的安全团队,它的配置也很常见。开 pull request 的代理需要一个能推送的令牌。最顺手的令牌就是你自己的 gh 登录,而 LiteLLM 的代理拿到的正是这个。
接着,令牌得放在代理读得到的地方:环境变量、配置文件、某个用来配置代理的 API 背后的数据库行。谁能读那个地方,谁就拿到令牌。这次,一把泄露过一次、从未轮换的 API 密钥就够了。
GitHub CLI 令牌让情况更糟。LiteLLM 指出,这些令牌“是在通过 2FA 之后签发的,不受 GitHub 针对组织或仓库敏感操作的‘超级用户’2FA 重新验证流程约束”。有了一个,攻击者就能关掉分支保护、force-push、批准发布,再抹掉证据。
在你自己的配置里找找同样的部件:代理环境里的 GH_TOKEN、含有 bearer 令牌的 MCP 配置、某天会有人提交出去的运行框架配置。LiteLLM 的 opencode.json 里不只有平台密钥,还有它的 GitHub、Linear、Slack MCP 服务器的 bearer 令牌,以及模型网关的密钥,全都是明文。
同一支队伍放在 Bromure 机器上
Bromure Agentic Coding 在你的 Mac 上以 Linux 虚拟机运行 Claude Code、Codex、Grok、Kimi 和 omp。它的自动化做的事和 LiteLLM 的代理平台一样:代理按计划或在出现新的 GitHub issue 时启动,在全新的 worktree 里工作,然后推送分支或开 pull request。拿 LiteLLM 的攻击链对一台运行这些自动化的机器走一遍。
代理拿到的是替身
在机器的 Credentials 中添加 GitHub 令牌。启动时,Bromure 会在虚拟机里写入一个假的:长度正确的 ghp_ 值,放在 GH_TOKEN、gh 的配置和 ~/.git-credentials 里。代理的 gh pr create 和 git push 照常工作。当请求离开虚拟机前往 github.com 时,Mac 上的代理服务器会把假的换成真正的令牌,而且只针对那个主机。
真正的令牌加密保存在 Mac 上,加密密钥保存在 macOS 钥匙串里。它从不写入虚拟机,不在任何文件,也不在任何变量。手册这样描述主机与虚拟机之间的通道:“虚拟机没有任何请求可以把真正的令牌要回来。”
所以,打开 LiteLLM 组织的那一步,在这里没有东西可返回。把代理的环境转储出来、读它的配置、通过 API 列出每个自动化:里面的 GitHub 值全是替身。把它粘贴进你自己的 gh,GitHub 会拒绝,因为只有那台机器的代理服务器知道它代表什么。
提交出去的配置也只有替身
这次泄露始于公开仓库中的一份运行框架配置。在 Bromure 机器上,代理读取的配置文件都是用替身写成的。在机器的 MCP 面板设置的 MCP 服务器 bearer 令牌,以 brm-mcp_… 值送到代理手上,在发往该服务器主机的请求中于传输途中替换。在 Preferences › Models 设置的模型密钥从不进入虚拟机;代理看到的是符合供应商格式的假值。其他任何 API 密钥放在 Other API keys,加上主机过滤,虚拟机会在你指定的变量里拿到一个 brm_… 值。
当代理不小心提交了自己的配置,或开发者把它复制到演示仓库,文件里带的都是替身。里面没有任何东西能在那台 Mac 以外使用,所以你会不会被入侵,不取决于一次 force-push,也不取决于一份没人做完的吊销清单。
没有平台 API 可调用
LiteLLM 的攻击者是从互联网访问到平台 API 的。Bromure 的自动化在你的 Mac 上运行。用手册的话说:“没有云服务,也没有入站网络攻击面。”GitHub 和 Linear 的触发器由 Mac 每三分钟用机器存储的令牌轮询一次,那个令牌留在主机上。没有 webhook 端点,没有主密钥,也没有泄露的密钥能打开的 /managed_agents 路由。
被盗的替身会触发警报
10 月 10 日的工作流把机密发到 webhook.site。设想同一招用在 Bromure 机器上:一个被投毒的依赖,或一段经由提示注入塞进来的指令,读取 GH_TOKEN 并把它发到 webhook.site。
代理服务器会检查每个出站请求的请求头和正文,找出被发往非其签发主机的替身。一个要发往 webhook.site 的 ghp_ 替身,会在任何字节发出前被 HTTP 451 拦下。虚拟机随即暂停,Mac 上弹出警告 This environment may have been compromised,列出令牌预览、凭据名称、它所属的主机,以及它被发往的主机。你可以关闭机器,或保存它的磁盘和主目录以供调查。
如果机器的沙箱策略把 Unmatched traffic 设为 Deny,webhook.site 根本连不上,这次尝试会在 Security Timeline 中显示为红色的一行。
推送会等你
新的 Bromure 机器启动时,GitHub 的写入策略是 Prompt before write。读取直接放行。git push 或任何对 github.com 的写入,都会停在你 Mac 上的对话框:
Allow write on "GitHub" from workspace "release-bot"?
POST /repos/<owner>/<repo>/git/refs
攻击者的每一步都是写入:ci-tmp- 分支、修改发布工作流、对 main 的 force-push、删除运行记录。从 Bromure 机器发出的话,每一步都会停在这个对话框。Allow once 只放行那一次写入,下一次推送会再问一次。force-push 在传输上仍是一次推送,Prompt before write 一样会拦。
无人值守的自动化没法等人点击,所以要给它刚好够用的最窄令牌,并套用为那台机器设置的策略。你添加到 release-bot 机器上的令牌,可以是只限单个仓库的细粒度令牌。你自己那个对一切都有所有者权限的 gh 登录,没有理由出现在那台机器上。
记录在你的 Mac 上
LiteLLM 在 GitHub 的审计日志里比对令牌哈希,才查出哪个被盗令牌做了什么。在 Bromure 机器上,每次在传输途中替换凭据,都会在 Security Timeline 留下一行 Credential brokering,附上打码后的令牌和主机。你拒绝的每次写入,都是一行红色的 Guardrails,附上方法和路径。在已注册到 bromure.io 的 Mac 上,同样的事件也会发送到你的组织,所以从意料之外的机器使用的令牌会自己浮现,不必有人翻遍审计日志。
代理拿到的是替身
GH_TOKEN、gh 的配置和 ~/.git-credentials 里放的是 ghp_ 假值。真正的令牌在 Mac 上换入,而且只针对 GitHub 的主机。
没有 API 会发放令牌
自动化在你的 Mac 上运行,没有入站攻击面。虚拟机能调用的任何东西,都不会返回真正的凭据。
泄露会让机器暂停
替身只要被发往自己主机以外的地方,就会被 451 拦下,虚拟机也会暂停,直到你做出决定。
写入前先询问
新机器在每次推送、每次写入 GitHub 之前都会询问,包括 force-push,并把确切的请求显示在屏幕上。
根本不要把令牌交给代理
LiteLLM 正在吊销所有令牌,并将在下周公布流程上的改变。轮换、收窄权限、服务账号都有帮助。但每一种做法仍会把一个能用的令牌留在代理读得到的某处,而那正是攻击者在 10 月 10 日来找的东西。
在 Bromure 机器上,代理用来工作的令牌,只能通过它自己的代理服务器、对它被签发的那一个主机生效。代理读得到的东西,接触到代理的攻击者也读得到;而在 Bromure 机器上,这些东西里永远没有真正的令牌。
想知道同样的替身如何对付盯上编程代理文件的窃密木马,请读窃密木马打包带走的是替身。关于代理留在自己对话记录里的副本,请读令牌留在了对话记录里。