这个包真的是 Red Hat 的
2026 年 5 月底到 6 月 1 日之间,一个名为 Miasma 的蠕虫把窃取凭据的代码推进了 @redhat-cloud-services npm scope 下的 32 个包里——那是 Red Hat 自己的命名空间,每周约 11.7 万次下载,由 Red Hat 真实的发布流水线签名。没有可供识别的 typosquat,也没有可供标记的未知维护者。信任信号就是 scope 上厂商的名字,而厂商的名字恰恰是攻击者乘虚而入所凭借的东西。这篇文章讲的是为什么「优先选择信誉良好的发布者」不再是一种防御,以及当执行安装的代理运行在按配置文件隔离的 Bromure VM 里时,什么会发生改变。
我们在五月写过的那些供应链攻击都有破绽。本该是版本范围的
地方却出现了一个 Git URL。一个 Bun 运行时凭空冒了出来。一个
故意安装失败的可选依赖。Miasma 没有破绽。那些包是
@redhat-cloud-services/sources-client 和它的另外三十一个
兄弟——货真价实是 Red Hat 的,在 Red Hat 自己的 npm scope 下,
由 Red Hat 自己的流水线发布,带着有效的签名。什么都没有伪造。
攻击者根本不需要伪造任何东西。scope 上的那个名字,就是整个
漏洞利用。
一个解析 @redhat-cloud-services/vulnerabilities-client 的编码
代理不会迟疑,换作你也一样。它是来自业界最大的企业厂商之一的
第一方依赖。没有维护者需要审查,因为维护者就是 Red Hat。没有
名字需要眯着眼去找有没有写错的字母,因为名字拼得分毫不差。一个
谨慎的开发者或一个谨慎的代理在运行 npm install 之前所应用的每
一条启发式规则,结果都是绿灯。于是安装运行起来,一个 preinstall
钩子触发了,而这个钩子是一个 4.2 兆字节的混淆 JavaScript blob,
一上来就开始读取文件系统找钥匙。
整起事件演示了一个让人不舒服的事实:信誉良好的发布者不是一种 安全控制。它感觉像是。过去三年里大多数供应链建议都倚仗着它。而 在 5 月 29 日,它什么都换不到。
Miasma 做了什么。
这场活动——按照
OX Security
的说法,字符串 Miasma: The Spreading Blight 最早出现在一个标注
日期为 2026 年 5 月 29 日的提交里——被 Aikido
和 OX Security 捕获,随后由 Socket、JFrog、Wiz、ReversingLabs、
Microsoft 等多方分析。BleepingComputer
和 The Hacker News
都在 6 月 1 日做了报道。
剥到机制层面的轮廓:
- 切入点是一个被攻陷的 Red Hat 员工 GitHub 账号,被用来把
恶意提交推进
@redhat-cloud-services的源码仓库里。 - 一个 GitHub Actions 工作流携带着一个
_index.js脚本,它用一个 OIDC 令牌向 npm 的可信发布端点认证——也就是 npm 如今相比长寿命 发布令牌更推荐的那套无密钥机制。从 npm 这一侧看,是 Red Hat 的 CI 发布了 Red Hat 的包。签名是真的。 - 发布出去的包携带着一个
"preinstall": "node index.js"钩子和一个 大约 4.2 MB 的混淆载荷。 - 安装时,载荷会搜刮 GitHub Actions secrets、AWS 凭据、Google Cloud
凭据、Azure 服务主体、HashiCorp Vault 令牌、Kubernetes 服务账号
令牌、npm 和 PyPI 发布令牌、SSH 密钥、Docker 凭据、GPG 密钥,以及
.env文件——然后把找到的一切加密后外泄。 - 它通过用窃取来的访问权限经由 GitHub API 提交来自我传播,用 GraphQL
读取
action.yml文件,再通过 mutation 把新的工作流写回去,于是 这些改动——用 Red Hat 自己提交日志里的话说——显示为已验证和 已签名。
总共有 96 个版本中的 32 个包中招,这些包每周大约有 11.7 万次
下载,而更广泛的这场活动波及了 309 个 GitHub 仓库。Socket 的
评估是,这*"实际上是一场迷你版 Shai-Hulud 活动:它使用了同样的核心
战术——安装期执行、凭据收割、CI/CD 瞄准、加密外泄,以及潜在的
下游传播。"* Red Hat 的声明是*"这些包严格限定于内部开发,恶意代码
从未发布给客户使用"*——这是真的,但对那些在包被撤回之前的窗口期内
把 @redhat-cloud-services/* 作为传递依赖拉下来的开发者和 CI runner
来说,也算不上多大安慰。
人人推荐的那个防御,正是失守的那个。
我们三周前写过一个类似的蠕虫
——TanStack 那次攻陷,破绽是一个挂在固定 Git URL 上的 prepare
脚本,和一个凭空冒出来的 Bun 运行时。那篇文章诚实的教训是:别信
lockfile,别信签名。Miasma 是同一颗螺丝再拧一圈,而把不同之处说
清楚很有必要,因为差异正是要点所在。
标准的供应链卫生栈有三级台阶。固定你的版本。验证来源。优先选择
信誉良好的发布者。Miasma 径直从这三级中间穿了过去。固定版本毫无
作用,因为恶意版本就是发布出去的那些版本。来源验证毫无作用,
因为来源是有效的——OIDC 可信发布流程货真价实就是 Red Hat 的 CI,
而在下游,蠕虫铸造出来的工作流提交被 GitHub 自己标记为已验证、
已签名。至于第三级台阶,优先选择信誉良好的发布者,在这里不只是
被击破——它本身就是攻击面。@redhat-cloud-services 这个 scope 的
信誉,正是这些包被不假思索拉下来的原因。命名空间越受信任,对接管
它的人来说就越有用。
不存在哪一个版本的「把包读得更仔细一点」能抓住这个。包没问题。包 是 Red Hat 的。问题在于带着开发者环境凭据的安装期代码执行才是 那份契约,而一个受信任的名字,丝毫改变不了那段代码一旦运行起来 能碰到什么。
同一个安装,发生在 Bromure Agentic Coding 里面。
Bromure Agentic Coding 让你的编码代理运行在一个
按配置文件隔离的 Linux VM 里——它有自己的内核、自己的文件系统、
自己的网络栈,跑在 Apple 的 Virtualization 框架之上。一个配置文件
就是一个连贯的工作范围:这个客户、这个内部产品、这个开源库。
代理在里面做它的 npm install,而主机——你真实的钥匙串、你真实的
云凭据、你真实的 SSH 密钥——在一道 preinstall 钩子无法逾越的、由
硬件强制的边界的另一侧。
凭据不住在配置文件里。它们住在主机上,在一个凭据代理
后面。当代理需要 push 一个提交或发布一个包时,它不会从访客文件系统
上读出一个令牌——压根没有令牌可读。它通过一个 Unix 域套接字,请求
代理替它使用某个凭据。代理持有真正的 GitHub App 私钥,铸造一个
短寿命的、scope 限定到代理本就在操作的那个仓库的安装令牌,并且
——对于配置为需要它的配置文件——弹出一个授权提示,由开发者在主机上
回答,请求才会发出去。令牌完全在主机这一侧被铸造并挂到出站请求上;
它从不进入访客的内存或磁盘。这条原则跟 ssh-agent 一样古老:中介
凭据的使用,绝不交出它的取值。
那么把 Miasma 的搜刮放进那道边界里走一遍。node index.js 读
~/.aws/credentials,找到一个存根或什么都没有。它读 ~/.npmrc,
找不到发布令牌。它在环境里找 $GITHUB_TOKEN,没有任何东西可偷
——安装令牌是在主机上铸造并花掉的,从不写进访客,而且只有当开发者
回答了授权提示时,代理才会铸造出一个。GPG keyring、SSH 私钥、Vault
令牌、kubeconfig:都在主机一侧,要么经中介暴露,要么根本不存在。
那个 4.2 MB 的载荷完全照设计跑到了结束。它只是外泄了一个从未持有
过开发者钥匙的访客而已。
那个被代理拒绝转发的 push。
窃取钥匙只是 Miasma 的一半。另一半是传播:它用窃取的 GitHub 访问 权限经由 API 把投毒工作流提交回去,正是这一点把一次糟糕的安装变成了 309 个仓库。凭据代理处理盗窃。Guardrails——在 Bromure Agentic Coding 2.0 中加入——处理滥用。
Guardrails 是一个主机侧的策略引擎,它就活在被中介的流量本就流经的
那同一个 MITM 代理里面,所以访客里被攻陷的代理无法绕过它。每个请求
都按它对资源实际做了什么来分类,而每个资源——GitHub、AWS、Kubernetes、
Docker 镜像仓库、DigitalOcean、GitLab、Bitbucket、托管数据库——都
可以设为关闭、拦截破坏性操作或只读。把一个配置文件的 GitHub
guardrail 设成只读模式,一个 git push——也就是蠕虫写回它工作流
所需的 git-receive-pack——会返回一个硬性的 403,而 git fetch
照常工作。对 Kubernetes API 的一个 DELETE、对镜像仓库里某个清单的
删除、对 EC2 的一个 Terminate* 调用:同样的待遇。代理看到的只是
一个普通的 API 失败。Miasma 的传播步骤是一个被代理拒绝转发的写操作,
不管代理有没有靠近过一个真实的凭据。
那持久化怎么办?
这是按配置文件模型对一项代价保持诚实的地方。Miasma 是一个蠕虫;它 全部的野心就是卷土重来。在一个一次性磁盘的幻想里,你可以把这事一挥手 带过——任务之后磁盘就没了。一个 Bromure 配置文件是长期存活的,所以 一个把自己写进配置文件内部某个启动脚本的载荷,可以存活到这个配置 文件里的下一个代理会话。我们不会假装不是这样。
不过那个持久化所继承的,是一个没有主机钥匙的访客,和一个只会说 短寿命、scope 受限令牌的代理。蠕虫在它死去的那同一个沙箱里醒来。它 能读到同样的存根。它能请求代理为这个配置文件被授权的那唯一一个仓库 使用某个凭据——而开发者仍然得回答那个授权提示,事情才能继续,并且 Guardrails 可以径直拒绝那个 push。它够不到主机钥匙串、其他配置文件, 或开发者的云凭据,因为那些东西从一开始就不在边界里面。持久化买到的 只是在一个空盒子里的持续存在。
而那当中的每一点——preinstall 的触发、node index.js 加载一个
多兆字节的 blob、写进某个启动路径的文件、那次出口尝试——都落进了
hypervisor 级别的会话追踪。
当 Aikido 在第二天早上发布指标时,「这个配置文件到底跑过 Miasma
没有?」这个问题是一次 grep,而不是一场事件响应行动。
这在哪些地方救不了你。
代理的 scope 就是全部赌注。
如果一个配置文件今天被配置为可以发布到你的 npm scope,而这个 配置文件今天安装了 Miasma,代理会让它发布。中介之所以有效,是 因为授权窄而短命。一个不需要发布的配置文件就不应该能发布。有意地 把它的 scope 限定好。
它不审查 diff。
Miasma 通过提交那些显示为已验证、已签名的工作流来传播。一个只读的 GitHub guardrail 会径直拦下 push——但一个正当需要 push 的配置文件 跑在拦截破坏性操作模式下,而 Guardrails 是按方法分类,不是按 diff 里有什么。无论是隔离还是方法级的 guardrail,都拦不住一个代理被 说服去提交一个它被允许 push 的投毒工作流。读 diff。追踪会告诉你 该读哪些 diff。
剪贴板默认共享。
Bromure 出厂时主机/访客剪贴板共享是开着的,因为把一段堆栈跟踪 粘进聊天是人类整天都在做的事。对于一个敏感的配置文件,隔离剪贴板。 这个控制是存在的;只是它不是默认值。
追踪是审计日志,不是 IDS。
会话追踪记录了 preinstall、那个 blob、那次出口。它本身并不判定 目的地是敌对的。它捕获到的内容足够多,所以一旦有人点出那个指标, 你的答案两秒钟就能找到。
下一个 scope 已经受信任了。
TanStack 蠕虫的教训是 lockfile 和签名都不是防御。Miasma 加上了那条
让人不舒服的推论:发布者也不是。@redhat-cloud-services 这个 scope
受信任并没有做错什么——受信任正是一个厂商命名空间的全部目的所在,
而这恰恰是它值得被攻击的原因。下一场活动会乘着一个你同样信任的
scope 而来,签名同样有效,由一个直到失守那一刻为止货真价实就是
厂商的流水线发布。
你没法靠更谨慎地去信任来修好这件事。你修好它的办法,是把事情安排成
让「这是哪个 scope 发布的」不再是你的钥匙串所依赖的那个问题。
Bromure Agentic Coding 就是这样一种配置:代理在
一个按配置文件隔离的 VM 里做它的安装,真实凭据待在主机上一个代理
后面,而一个 preinstall 钩子最坏能做的,就是外泄一个从未持有过你
钥匙的盒子。它免费、开源,今天就发布。