返回所有文章
发布于 · 作者 Renaud Deraison

这个仓库真的是 Microsoft 的

2026 年 6 月 5 日至 6 日,Miasma 蠕虫把窃取凭据的代码推进了 Microsoft 自己的四个 GitHub 组织——Azure、Azure-Samples、microsoft、MicrosoftDocs——下的 73 个仓库里,其中包括官方部署 Action 的 Azure/functions-action,以及 durabletask 这个早在 5 月就已经被清理过一次的仓库。这一次,载荷没有等到 npm install。当一名开发者在 Claude Code、Cursor、Gemini CLI 或 VS Code 里打开这个仓库的那一刻,它就触发了。这篇文章讲的是为什么信任信号——「它是 Microsoft 的仓库」——又一次成了攻击面,以及当打开它的代理运行在一个按配置文件隔离的 Bromure VM 里、在一个凭据代理、一道读写护栏和一段包冷却期之后时,什么会发生改变。

一周前,这个蠕虫乘着 Red Hat 的 npm scope 而来,通过一个 preinstall 钩子触发。这一周,它乘着 Microsoft 的 GitHub 而来,根本不需要任何安装。Miasma 把项目级配置植入了 73 个 归 Microsoft 所有的仓库——其中就有官方部署 Action Azure/functions-action——而当一名开发者在 Claude Code、 Cursor、Gemini CLI 或 VS Code 里打开这个仓库的那一刻,载荷 就执行了。触发点不是克隆,而是在你的代理里打开它。

一名为了调试一次失败的部署而克隆 Azure/functions-action 的开发者 不会迟疑,他指向这个仓库的代理也一样。它是来自 Microsoft 自己的 Azure 组织的第一方仓库——半个生态系统都以 Azure/functions-action@v1 引用的那个 GitHub Action 的规范源头。没有维护者需要审查,因为维护者 就是 Microsoft。没有名字需要眯着眼去看,因为名字分毫不差就是它该有 的样子。于是仓库被打开了,代理像每一个现代编码工具在打开文件夹时 所做的那样读取项目的配置——而其中一个配置文件指向一条命令,这条 命令是一个约 4.3 兆字节的 blob,一上来就开始读取文件系统找钥匙。

我们七天前写过这同一场活动的 npm 那一半。Miasma 是同一颗蠕虫 ——是 TeamPCP 在 5 月中旬公开发布的 那段「Mini Shai-Hulud」代码的一个变种——而它所演示的那个让人不舒服 的事实也是同一个,只是再拧了一圈:信誉良好的来源不是一种安全控制。 它感觉像是。大多数供应链建议都倚仗着它。而在 6 月 5 日,它什么都没 换到,而且是两重意义上的什么都没换到:命名空间是 Microsoft 的,而 执行并非来自你选择安装的某个包。它来自打开一个文件夹。

Miasma 对 Microsoft 的仓库做了什么。

2026 年 6 月 5 日至 6 日,在恶意提交被推进 Microsoft 的仓库之后, GitHub 禁用了 Microsoft 四个 GitHub 组织下的 73 个仓库——这一点 见 The Hacker News 以及 StepSecurity 的详细拆解。Redmond Magazine 在 6 月 8 日做了报道。具体分布如下:

  • Azure —— 49 个仓库,其中包括 Azure/functions-action(官方的 Functions 部署 Action),以及 .NET、Python、Java、Go 和 PowerShell 的语言 worker。
  • microsoft —— 10 个仓库。
  • Azure-Samples —— 13 个仓库。
  • MicrosoftDocs —— 1 个仓库。

剥到机制层面的轮廓:

  • 切入点是一个此前被攻陷的、拥有提交权限的贡献者账号,被用来 把恶意提交直接推进这些仓库——并打上 [skip ci] 标记,让这些改动 绕过本来会运行的 CI/CD 检查。
  • 这次提交植入了项目级配置——也就是当你打开文件夹时,一个编码 代理或 IDE 会自动读取并据以执行的那类文件:一个编辑器任务、一个 代理钩子、一个项目定义的 MCP 服务器。这正是 Adversa AI 的 TrustFall 在 Claude Code、Cursor CLI、Gemini CLI 和 Copilot CLI 上演示过的 同一类信任边界——这四者都会在文件夹信任提示之后立即执行项目定义 的配置。
  • 载荷——约 4.3 MB 的混淆代码——会在仓库被在 Claude Code、 Gemini CLI、Cursor 或 VS Code 里打开时执行,或在通过一个 npm test 脚本运行时执行。不是仅凭克隆。 把你的代理指向那棵 被克隆下来的目录树,才是运行它的动作。
  • 一旦执行,它就在主机上搜刮 GitHub 令牌、AWS 密钥、Azure 服务 主体、GCP 凭据、npm 和 PyPI 发布令牌、SSH 密钥,以及 .env 文件,然后用窃取到的访问权限把自己提交到下一处——正是这一点 让它成了一颗蠕虫,而不是一次性的攻击。

有一个细节值得多停留一下:Azure/durabletask 也在中招的仓库之列 ——而它在 5 月的 TeamPCP 活动中就已经被攻陷过一次并被清理。一个 修复过一次的仓库,在五周之后又被重新投毒。清理不是一个你达成之后 就能保住的状态;它是一个状态,链条里另一个凭据一旦被夺走,你就会 从中跌落出去。

同样值得精确说清楚的,是哪些事没有发生。Microsoft 的企业网络 没有被攻破。Azure 这个云服务没有被攻破。没有任何客户数据、没有任何 生产系统被触及。这是一次针对源码仓库的攻击——而它波及面最广的 后果跟恶意软件本身毫无关系:在 GitHub 禁用 Azure/functions-action 的那一瞬间,地球上每一条引用了 Azure/functions-action@v1 的流水线 都解析不出来了。Microsoft 是最高调的携带者。真正被攻陷的,是那些 在 6 月 3 日到 5 日之间在代理里打开了被投毒仓库、凭据被从自己机器上 搜刮走的开发者。

开发者笔记本 — 主机密钥对代理在打开文件夹时所运行的任何东西可见MICROSOFT 自己的 GITHUB被攻陷的贡献者账号 → push [skip ci]Azure/functions-actionAzure/durabletask(又一次)+ 植入项目配置开发者克隆git clone …/functions-action尚无任何东西运行第一方仓库 ⇒没有迟疑的理由在编码代理里打开 — 载荷触发Claude Code · Cursor · Gemini CLI · VS Code打开文件夹 → 读取项目配置代理钩子 / 任务 / MCP 服务器↳ 执行 4.3 MB 载荷(仅凭克隆并不会运行它)主机文件系统 & ENV — 全部真实,全部可被载荷读取$GITHUB_TOKEN, gh hosts.ymlpush 访问(真实)~/.aws, Azure 服务主体云密钥(真实)~/.npmrc, PyPI 令牌在此发布(真实)~/.ssh/id_ed25519, .env密钥 + CI secrets(真实)tar | encrypt | exfiltrate→ 从你的机器上被搜刮走自我传播窃取的 GitHub 访问提交到下一个仓库植入同一份配置73 个仓库, 4 个组织durabletask: 两次中招
Miasma 出现在一台在编码代理里打开了某个受影响的 Microsoft 仓库的开发者机器上。一个此前被攻陷的贡献者账号推送一个 [skip ci] 提交,植入项目级配置——一个代理钩子、一个编辑器任务、一个 MCP 服务器定义。开发者克隆这个仓库(什么都不会运行)并在 Claude Code、Cursor、Gemini CLI 或 VS Code 里打开它。打开文件夹时,代理读取并据以执行那份配置,执行一个 4.3 MB 的载荷,它在主机上搜刮 GitHub 令牌、AWS 密钥、Azure 服务主体、npm/PyPI 令牌、SSH 密钥和 .env 文件,加密后发送出去,并用窃取的 GitHub 访问权限把自己提交进下一个仓库。没有 typosquat,没有伪造的维护者,没有 npm install。信任信号是 Microsoft 的组织名,而执行来自打开一个文件夹。

同一个仓库,在 Bromure Agentic Coding 里面打开。

Bromure Agentic Coding 让你的编码代理运行在一个 按配置文件隔离的 Linux VM 里——它有自己的内核、自己的文件系统、 自己的网络栈,跑在 Apple 的 Virtualization 框架之上。一个配置文件 就是一个连贯的工作范围:这个客户这个内部产品这个你为了 调试部署而克隆下来的开源库。你把 Azure/functions-action 克隆进 那个配置文件,并在里面用你的代理打开它。打开文件夹的触发点完全 照设计触发。载荷运行起来。然后它跑去一台它够不到的主机上找钥匙。

因为凭据不在配置文件里。这个 VM 出厂自带存根——对 gitghawskubectlnpm 以及任何期待一个 Authorization 头的东西 来说看起来都像真的假令牌。你的 Mac 上的一个代理坐在每一条离开 沙箱的连接前面,识别出那个存根,并在请求离开时在线路上把它换成 真正的密钥(那个握着钥匙的沙箱 详细讲了这套机制)。真正的 GitHub PAT、真正的 AWS 密钥、真正的 Azure 主体——它们没有一个会触及 VM 能读到的某个文件、某个环境变量 或某一页内存。SSH 密钥根本不会离开 macOS 钥匙串;只有 ssh-agent 套接字被转发进去,正如 OpenSSH 一向所设想的那样。

那么把 Miasma 的搜刮放进那道边界里走一遍。载荷在环境里找 $GITHUB_TOKEN,找到一个存根。它读 ~/.aws,什么都找不到。它读 ~/.npmrc,找不到发布令牌。它读 ~/.ssh,找不到密钥文件——那里有 一个被转发的套接字,而不是磁盘上的一个私钥。那个 4.3 MB 的 blob 完全照所写的跑到了结束。它只是外泄了一个从未持有过你钥匙的盒子, 身处一道由硬件强制的边界的错误一侧,跟一切要紧的东西隔着这道边界。

按配置文件隔离的 BROMURE VM代理打开 functions-action项目配置 → 载荷触发运行,但只在访客内部载荷看到的东西$GITHUB_TOKENstub_7f3a…~/.aws, ~/.npmrc存根 / 不存在~/.ssh 密钥文件只有套接字传播: git push(自己)需要写 → 向代理请求外泄主机钥匙: 无物可取需要一个依赖?npm install → 拉取离开 VM经由 Bromure 的代理代理 · 你的 MAC凭据代理真实 PAT / 密钥存于此存根 → 真实, 在线路上取值从不进入 VM护栏: 读/写push = 改动 → 提示点名动词 + 目标供应链OSV + socket.dev 扫描冷却期: < 2 天的隔离剥除安装脚本结果主机钥匙:搜刮到的是存根,真实的丝毫未动为传播而 push:暂停, 你说不新鲜的坏包:从不抵达 VM爆炸半径 = 1 个配置文件
三层防护,按 Miasma 命中它们的顺序排列。(1) 供应链:代理把每一次包拉取都对照 OSV 和 socket.dev 扫描,并把发布时间短于冷却期的版本隔离——这样在生态系统还没反应过来的时候,代理连一个新鲜的恶意依赖都拉不下来。(2) 凭据中介:VM 只持有存根;真实的密钥待在主机上代理后面,在线路上换入,于是载荷在主机上的搜刮只找到占位符。(3) 护栏:蠕虫的传播步骤——一次为了把自己提交出去的 git push——是一个改变状态的调用,而一道读写护栏在线路上把它拦下并询问,点名那个动词和那个目标。每一层都在代理之下、在代理无法绕行的 VM 边界处强制执行。

传播步骤是一次写操作,而写操作要经过一个提示。

窃取钥匙只是 Miasma 的一半。另一半是扩散:它用窃取的 GitHub 访问 权限把自己提交进下一个仓库,正是这一点把一小撮被投毒的仓库变成了 73 个。即便是在一个正当地拥有 push 权限的配置文件里——比如你克隆 functions-action 恰恰是因为你打算对它提一个 PR——蠕虫的传播步骤 仍然得经由代理走出去,而那正是 Guardrails 迎面拦下它的地方。

Guardrails 读的是操作,不只是连接——它能分辨读和写。一个 git fetch 是读;一个 git push 是写。把一个配置文件的 GitHub 凭据设为写时询问,那么当代理伸手去做一个改变状态的调用的那一刻 ——蠕虫提交它配置所需的 git-receive-pack、对某个 API 的一次 DELETE、对 EC2 的一次 Terminate*——Bromure 就在线路上把它拦下, 并在你的 Mac 上弹出一个提示,点名那个动词、那个目标和那个配置文件。 你给出的授权是有时限的:发布一次给十五分钟,吓人的那些只给一次性, 要是这个请求毫无道理就永不给予。读操作从不打断你;代理整天 fetch、 grep、读取都行。会暂停的是那个改动

这就是「代理有一个令牌」和「代理可以拿这个令牌为所欲为」之间的差别。 Miasma 整套扩散机制就是一次代理从没告诉你它在做的写操作——而一次 代理从没告诉你它在做的写操作,恰恰就是读写提示被造出来要抓住的东西。 那次传播蠕虫的 push 变成了一个你点不允许的对话框,正如「代理删掉了 生产数据库」不再是一份事后剖析 而变成了一个被你回绝的提示。

那个版本只有几小时大,而 Bromure 让包变老。

Miasma——以及更广的 Mini Shai-Hulud 谱系——还有第二条触达开发者的 路径:不是通过一个你打开的仓库,而是通过代理在干活时安装的一个 刚被投毒的。这场活动的 Red Hat 那一半正是如此,一个挂在某个 受信任 scope 下 32 个包上的 preinstall 钩子。而那些事件里残酷的 细节是时机:一个被攻陷的版本通常会在几小时内被发现并撤下——但那 正是一个自主运行、无人值守的代理可能把它拉下来的几小时。

Bromure 的供应链层把那同一个边界代理变成一个扫描检查点,而它 做了两件真正能对抗当日攻陷的事:

  • 它强制把每一次拉取都对照 socket.dev 以及 OSV 扫描。 OSV 抓住 高于你所设严重度阈值的已知 CVE。socket.dev 抓住漏洞数据库还没 跟上的东西——流氓安装脚本、行为型恶意软件、typosquat,以及刚刚 发布的攻陷。一个被标记的版本会在 tarball 落进 VM 之前就被拦下。 关键在于这个扫描运行在代理之下、在边界代理处:无论代理怎样 重写自己的配置来绕开你,拉取仍然要经由那道它跨不过去的边界离开。
  • 它强制执行一段冷却期。 Bromure 把过去两天内发布的任何版本 ——可调——都隔离起来,于是一个一小时前上传的版本,在那个配置文件 里就根本装不上,直到生态系统跟上为止。面对一个全部机会窗口 就是发布撤下之间那段空隙的蠕虫,冷却期并不是一个关于某个 包看起来坏不坏的启发式判断。它是一种拒绝——拒绝当那第一个去探明 真相的人。把它跟 Bromure 即时做的安装脚本剥除结合起来——把 postinstall 钩子从 tarball 里拽出来并修正元数据哈希,让安装 仍能通过校验——那个真的落地的包,落地时就是惰性的。

对 Miasma 而言,仓库打开这条路径是头条。但同一场活动也会通过包来 扩散,而冷却期正是那个本可以让它的 npm 那一半挨饿的控制:一个新鲜的 @redhat-cloud-services 版本,或一个在调试那个 Microsoft 仓库时被 拉下来的、刚被投毒的传递依赖,会在它危险的那几个钟头里一直待在隔离区。

这在哪些地方救不了你。

一个你批准的 push 就是一个真的发生的 push。

读写护栏抓住的是代理没告诉你的那个写操作。它不读 diff。如果你 正当地在向 functions-action push,而你批准了那个提示,Bromure 就会转发这次 push——原则上也包括一个你在 diff 里没注意到的被 投毒的工作流。读你所批准的东西。 会话追踪会告诉你该读 哪些 diff。

冷却期是一扇窗,不是一堵墙。

两天是按观察到的发布-撤下间隙调出来的,但一个有耐心的攻击者可以 在一个被攻陷的版本上坐得比冷却期更久,到第三天仍然可被安装。 冷却期让当日蠕虫挨饿;它并不为一个仅仅是变老了的包做担保。 socket.dev 和 OSV 仍然得各尽其职。

配置文件是长期存活的,所以持久化会持续。

一个 Bromure 配置文件不是一块一次性磁盘。一个把自己写进配置文件 内部某个启动路径的载荷,可以存活到这个配置文件里的下一个会话。 它醒来时面对的,是一个没有主机钥匙的访客,和一个只会说短寿命的、 需经提示的、scope 受限令牌的代理——身处一个里头空无一物的盒子里 的存在——但终究是一种存在。

有意地把代理的 scope 限定好。

如果一个配置文件今天被配置为可以 push 到某个仓库,而这个配置文件 今天跑了 Miasma,一次被批准的写就会通过。中介授权之所以有效,是 因为它们窄。一个只需要读某个仓库的配置文件,就不应该能写它;一个 从不发布的配置文件,就不应该持有任何发布令牌。隔离遏制爆炸;scope 限定决定它本来最大能有多大。

下一个受信任的仓库已经被克隆到了某处。

TanStack 蠕虫的教训是 lockfile 和签名都不是防御。Red Hat scope 的教训是发布者也不是。Microsoft 加上了下一条推论:仓库也不是, 而且触发点甚至不必是一个你选择的安装——它可以是一个你的代理打开的 文件夹。Azure/functions-action 这个仓库因为受信任而并没有做错什么。 受信任正是一个规范的第一方 Action 的全部要点所在,而这恰恰是它值得 被投毒的原因——就 durabletask 而言,还被投毒了两次。

你没法靠更谨慎地去信任来修好这件事,因为这份信任从来没有错付。你 修好它的办法,是把事情安排成让「这是哪个仓库」和「这是哪个 scope 发布的」不再是你钥匙串所依赖的那些问题。Bromure Agentic Coding 就是这样一种配置:代理在一个按配置文件隔离 的 VM 里打开仓库,真实凭据待在主机上一个代理后面,代理做出的每一次 写都得过一个提示,而一个包在熬过冷却期之前根本装不上。一次被投毒的 文件夹打开,最坏能做的,就是外泄一个从未持有过你钥匙的盒子。它免费、 开源,今天就发布。