构建把密钥一起发布了出去
Beacon CRM 的扩充版事件报告,把整份客户数据库被窃——涉及超过 1,500 家英国慈善机构——追溯到一把 AWS 访问密钥:那把密钥是被它自己的构建流程烤进一个公开 JavaScript 文件里的。没有人闯进开发者的机器。一个构建工具把环境变量复制进了产物,而那正是构建工具存在的意义。Bromure Agentic Coding 的答案是:那个变量里没有任何值得复制的东西。
这个故事的前半段里没有攻击者。一个构建流程把秘密从环境变量复制进了一个 JavaScript 文件,而一台 Web 服务器把那个文件交给了每一个开口索取的人。窃取真正 发生时,只是一个 GET 请求。
Beacon 是英国慈善机构用来管理捐赠人、支持者和志愿者的 CRM。8 月 12 日,其 CTO David Simpson 就公司在 8 月 4 日首次披露的一起数据泄露,发布了扩充版的事件报告。 The Register 第二天做了报道, SecurityWeek 在 8 月 14 日跟进, 而根本原因只有一行:
一把可能暴露在公开 JavaScript 构建产物中的 AWS 访问密钥。
7 月 27 日 01:20:16 UTC,有人开始使用那把密钥,并持有访问权限一小时二十七分钟。 Beacon 对流出内容的评估是:
一份包含 Beacon 全部客户数据的数据库副本,连同附件文件,已被制作,并极有可能已 被威胁行为者以可读格式下载。
这涉及超过 1,500 家机构:支持者姓名、电话号码、邮箱地址、通信地址、捐赠记录和附 件文件。没有卡片或银行信息,因为 Beacon 的客户并不把那些存在里面。 ICO 审视了至少一家受害机构, 认定该机构对这次泄露不负任何责任。这个结论是正确的,同时对一个不得不向支持者解释 家庭住址去了哪里的筹款团队来说,也是一种冰冷的安慰。
Beacon 对静态数据做了加密。那什么也没改变。 Cybersecurity News 指出, AWS 会代表任何持有有效凭据的人解密。静态加密保护你的是有人把硬盘搬走的情形,它对 一个握着密钥的调用方没有任何意见。
值得盯着看的那一段
带着找出入侵的心思去读这份事件报告,你不会找到入侵。没有钓鱼邮件,没有被攻陷的维
护者,没有被投毒的依赖或提示词注入,工程师的笔记本上也没有恶意软件。这个故事里没
有任何东西绕过了控制,因为根本没有人从任何地方把凭据抽出来。Beacon 的构建流程把一
个值从环境变量复制进打包产物,Beacon 把那份产物作为静态资源部署,而 Web 服务器按
照设计,把它交给了每一个发起请求的客户端。外泄通道是一个 <script> 标签。
这个机制一点也不奇特。前端构建是刻意把环境变量内联进去的,因为浏览器在运行时根本 没有环境可读:
- Vite 会在构建期把任何以
VITE_为前缀的变量代入import.meta.env。Next.js 对NEXT_PUBLIC_做同样的事,Create React App 用的是REACT_APP_。前缀就是你用 来请求这项替换的方式。 - webpack 的
DefinePlugin和 esbuild 的--define会把你源码里的一个符号替换成 字符串。它们没有“哪些字符串是秘密”的概念,也无从获得这种概念。 - source map 是第二份副本。服务端渲染的框架再加上第三条路径:一个在代码中被读取、 最终落进客户端组件的值,会被序列化进负载里。
这些副本每一份都来自那个启动了构建的人——或那个东西——的环境。环境是原料,而构建是 一台把原料抄进一个你随后会发布的文件的机器。
这一切开始的那台工作站
Beacon 的泄露,就后果而言是一个云的故事。它的成因坐落在一台开发者工作站上,而工作 站正在改变。
一个处理工单的智能体,在一个平常的下午就能把这些全做完。它加上一个环境变量,好让
某个功能能连到某个服务。它编辑 vite.config.ts 或 next.config.js。它写下读取那
个变量的那一行,并挑选那一行所在的模块——而这个挑选,正是决定那个值留在服务端还是
跨进打包产物的判断。它运行 npm run build。当部署目标希望产物被纳入版本控制时,它
就把 dist/ 提交上去。
这里没有一件事需要智能体犯错,而就算它犯了错,你也得不到任何信号。构建会成功,因 为字符串就只是字符串。打包产物以压缩成一行的形式发布,所以差异里没有任何东西会刺 到你的眼睛。那个值能用,所以功能能跑,工单就关闭了。
与此同时,智能体运行所在的工作空间里塞满了凭据,因为凭据正是让它有用的东西:云密 钥让它能查看一个存储桶,GitHub 令牌让它能推送分支,还有镜像仓库和数据库的凭据、一 把模型 API 密钥。这些每一样都是一串放在环境里的字符串,而构建流程会把那个环境整份 读完。
Beacon 的报告对这个安排提出了一个直接的问题。你的工作空间里的某个东西,迟早会把你 的环境复制进一个文件。问问你自己,它会拿到什么。
在 Bromure 的工作空间里,它拿到一个区域字符串
Bromure Agentic Coding 让智能体跑在 Apple Virtualization 框架上的一台用完即弃的 Ubuntu 虚拟机里,并以一个主机侧的 MITM 代理作为它通往网络的唯一路径。 凭据设计由此推出:真正的秘密留在 你的 Mac 上,虚拟机拿到的是看起来正确、实际一文不值的值。
AWS 有自己的处理方式,因为 SigV4 从不传输秘密本身。SDK 在客户端消耗秘密来计算 HMAC,放上线路的是签名,因此 Bromure 无法像处理 bearer 令牌那样,在传输途中把假值 换成真值。它转而移动签名发生的位置。
先从一个构建流程会找到什么开始。Bromure 导出到虚拟机里的是 AWS_DEFAULT_REGION
和 AWS_REGION,没有任何密钥、秘密或会话令牌。理由就写在源码里,在
SessionDisk.swift 中:
我们在这里“绝对不能”导出
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKEN:在 SDK 的链条中,环境变量会胜过credential_process,而 且它们还会摧毁“磁盘上不存在秘密”这项保证(环境变量会经由/proc、ps -E和 shell 历史泄漏)。
这份泄漏渠道清单描述的是进程窥视。一个读取 process.env 的打包器也属于这份清单,
而同一个决定就把它涵盖了。环境中根本不存在 AWS_SECRET_ACCESS_KEY 可供构建内联,
所以 DefinePlugin 什么也没替换,import.meta.env 什么也没带,而
NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY——刻意犯下的 Beacon 式错误——发布出去的是一个空
字符串。
SDK 仍然能工作。~/.aws/config 指向一个 credential_process 辅助程序:
[default]
credential_process = /mnt/bromure-meta/bromure-aws-creds.py
region = eu-west-2
该辅助程序从一个 socket 读取一份 JSON 文档。它返回真正的 AccessKeyId——那是用来
标识你、而不是验证你的东西——并搭配一把 Bromure 为本次会话生成的
SecretAccessKey:四十个字符,取自真正 AWS 秘密所使用的字符集,因此 boto3、
aws CLI 和 Terraform 都会接受它,并照常签名。而它们产生的签名,注定会失败。
接着由主机把它修好。AWSResigner 会识别任何经由代理发出的 *.amazonaws.com 请
求,剥掉来宾侧的 Authorization 头,并用只存在于主机进程地址空间中的凭据重新计算
SigV4;当配置文件带有 STS 材料时,还会加上真正的 X-Amz-Security-Token。你的
terraform apply 能跑,而秘密从未进入执行它的那台机器。
绕过代理,AWS 会这样回答:
An error occurred (InvalidSignatureException) when calling the
ListBuckets operation: The request signature we calculated does not
match the signature you provided.
凭据是向关闭的方向失效。一把只有经由你的 Mac 转发才能工作的凭据,无法在陌生人的笔 记本上被行使——而这正是 Beacon 那把被公开的密钥所缺少的性质。
盘面上其余的全是诱饵
AWS 是特例。至于其余部分,由线上边界来完成工作:你配置的每一份凭据,在虚拟机里都 以保留结构的假值现身,由真值加上每次安装专属的 32 字节盐,经 HKDF-SHA256 推导而 来。真值加密后留在你的 Mac 上,代理则在字节已经离开虚拟机之后,才把真值换上请求, 而且只在请求的目的地正是那份凭据被铸造给的主机时才这么做。
这让“某个工具把环境复制到了它不该去的地方”这一整类问题,都以同样的方式解决,无论 那个工具是什么:
模型与厂商 API 密钥
ANTHROPIC_API_KEY 是 sk-ant-api03-brm-…。OPENAI_API_KEY 是 sk-brm-…,
XAI_API_KEY 是 xai-brm-…。保留结构,所以各家 CLI 都会毫无怨言地接受,而
在其他任何地方都是惰性的。
Git、镜像仓库与云令牌
GH_TOKEN 是 ghp_ 加 36 个字符,GitLab 是 glpat- 加 20 个,DigitalOcean
是 dop_v1_ 加十六进制。~/.git-credentials、~/.docker/config.json、
~/.kube/config 与 ~/.config/doctl/config.yaml 全都存在、全都有内容、也全
都是假的。
一个 NEXT_PUBLIC_ 前缀
在任何一个前面加上它,打包器就会照你说的做。发布出去的产物带的是
brm-docker-… 或 ghp_ 形状的填充物,而这个错误让你付出的是一次重新部署,
而不是一份泄露通知。
被提交的 dist/ 或 source map
答案一样,而且不必指望任何人注意到。那份产物可以躺在公开仓库里、被索引、被所 有还在跑的秘密爬虫刮走,而它们收集到的字符串什么也验证不了。
Bromure 每次都以同样的方式推导出假值,因此一个会给自己的密钥取指纹的工具——像 Claude Code 缓存密钥哈希时所做的那样——永远不会看到凭据在会话之间改变。这里也没有 可供遗忘的开关。代理是虚拟机唯一的对外路径,所以绕过它的请求带着的是占位值,并会 在上游失败。
被公开的诱饵是一条绊线
Beacon 的密钥在 7 月 27 日之前,已在一个公开文件里待了不知多久,而任何人得到的第一 个信号,是事后才被读到的 7 月 27 日与 28 日的 AWS Cost and Usage 报表 出现尖峰。
Bromure 的入侵检测看守的是 你的 凭据,而不是目的地的声誉,这正是它能对一个从没 有人点过名的主机触发的原因。一部由工作空间自己铸造的假值所构建的 Aho-Corasick 自动 机,会扫过每一个外发请求的头部与正文。一个假值前往它被铸造范围之外的主机,就算作 外泄尝试,而 Bromure 的响应是:
- 代理以 HTTP 451 拒绝该请求,一个字节也不会抵达目的地。
- Bromure 当场暂停该虚拟机。
- Bromure 发出告警,点名该凭据、它被铸造给的主机,以及观察到它正前往的主机。
Bromure 随后把工作空间标记为已遭入侵,因此下一次启动需要抹除虚拟机的磁盘镜像和持久 家目录。你的令牌、SSH 密钥和设置会存活下来。
把这个机制对准 Beacon 的情形。一个为某个目的地铸造的假值,出现在发往另一个目的地的 请求里,这就是特征;而那个假值是怎么跑出去的并不重要:外流的打包产物、被偷的 dotfile,或一个带着浏览器、好奇心重的陌生人。第一个试用他捡到的凭据的人,会在他试 用的那一刻,在一份属于你的日志里自报家门。
Beacon 说它永远不会有的那份记录
Simpson 以 Beacon 所能确立事实的界限,作为事件报告的收尾:
关于这起事件,有些事我们也许永远无法查明。
Beacon 把这条界限说得很直白。具体是哪些对象、下载的确切目的地,以及哪些对象被访问 过的确定归属,都无法从现有的日志中判定。Beacon 认为整个数据库都流了出去,这是 从一份账单报表的形状推论出来的:Cost and Usage 数据里的一个传输量,与 Beacon 所存 储内容的大致体量相符。那是在无米下锅的情况下做出的出色取证工作,而现在每一家正在 给支持者写信的机构,都倚赖着它。
一个 Bromure 工作空间,把那份记录当作它运行方式的副产品生产出来。每一个请求都穿过 主机代理,所以代理会把发生的事写下来:
$ bromure-cli trace ls
HOST METHOD STATUS MS FLAGS
api.anthropic.com POST 200 412 swap×1
s3.eu-west-2.amazonaws.com GET 200 88
registry.npmjs.org GET 200 31
api.github.com POST 201 140 swap×1
trace hostnames 列出一次会话接触过的每一个不同主机,trace summary 把整批汇
总,trace leaks 显示未受管理的凭据。Trace Inspector(⇧⌘I)提供同样的视野,还带
上请求正文,而安全日志(Window → Supply Chain Log…)会在供应链与 451 的判定发
生的当下逐条追踪。Bromure 把这一切都以保险库主密钥加密,静态保存在你的 Mac 上。
那些 Beacon 只能靠推论回答的问题,在你这里是一次你自己执行的查询:这个工作空间跟什 么说过话、什么时候、带着什么,以及有没有任何长得像凭据的东西离开过。针对你自己的 数据,在你第一次起疑的那一分钟里。
把缝隙关上的那个安排
Beacon 的工程师没做任何不寻常的事。把凭据放进环境变量是被推荐的做法,而在构建期间 读取环境变量就是构建在做的事。一整份客户数据库,从这两件合理的事之间的缝隙里穿了 过去,而再多的小心也关不上它,因为小心是对注意力的一种指望,而不是一项控制。
那项控制,是把事情安排成让副本一文不值。把真正的凭据留在主机上,交给工作空间一个能 满足每一个读取它的工具的占位值,在运行代码的那台机器之外进行签名与替换,并且自己保 留每一个离开的请求的记录。这样一来,一个尽职工作的构建工具、一个做出看起来合理的修 改的智能体,以及一个从容读着你打包产物的陌生人,最后都会抵达同一个地方:一串在你的 Mac 之外毫无意义的字符串。
来源:The Register,“AWS key exposed in JavaScript may have lit way to Beacon's charity data”(2026 年 8 月 13 日) · SecurityWeek,“Over 1,000 Charities Hit by Beacon CRM Data Breach”(2026 年 8 月 14 日) · Infosecurity Magazine,“Exposed AWS Access Key Linked to Data Breach Affecting 1500+ UK Charities” · Cybersecurity News,“Beacon CRM Confirms Full Database Theft After AWS Access Key Breach”