我们本来准备把自己和客户的数据交给 Baseten。为稳妥起见,我们先用 Strix 对它做了一次安全检查。大约 25 分钟后,Strix 就拿到了一个有效的 GitHub 令牌,对 Baseten 的内部仓库拥有仓库级管理员权限。
我们开发的 Strix 是一个自主黑客智能体,所以当然需要(便宜又快速的)推理服务。我们当时在比较各种选择,Baseten 是显而易见的选项之一。它是个很出色的产品,估值达 130 亿美元,许多正经公司都依赖它。
但是……我们是一家安全公司。在把数据、模型或代码交给第三方之前,我们都会先扫描对方。我们宁可在开始依赖某项服务之前就发现问题并帮助修复(我们对几乎所有供应商都这么做,而且发现严重问题的比例很高)。
于是……我们让 Strix 对准 *.baseten.co,在没有任何凭证和源代码的情况下放手去跑。
它带回了一个属于 basetenbot 的有效 GitHub 个人访问令牌。这个令牌对 Baseten 的主产品仓库、驱动其集群的 GitOps 仓库以及 Homebrew tap 拥有管理员和推送权限,此外还对其他私有仓库拥有读写权限,其中包括针对特定客户的仓库。
这个镜像构建于 2023 年 3 月,而我们在 2026 年 7 月发现它时,令牌依然有效。
不过在讲细节之前,先要给 Baseten 的安全团队点个赞。他们确认该问题为严重级别,锁定了镜像仓库项目,并在第二天下午就轮换了令牌。他们专业而且响应极快(这类情况下往往并非如此)。
Strix 究竟是怎么发现的
Strix 的起手式和任何一次好的渗透测试一样:侦察。很多时候,你基础设施上最严重的漏洞,可能出在某个被你遗忘的子域名下的服务上(这也是我们始终建议在有代码访问权限的测试之外再做黑盒测试的原因)!于是 Strix 枚举主机、查阅证书日志,绘制出完整的攻击面。最终,它在 gcp-us-east4-zlw.registry.baseten.co 找到了一个 Harbor 镜像仓库。
Harbor 用来存储容器镜像,并把仓库按项目分组。其中一个项目是公开的。无需任何令牌或认证,Strix 就能列出仓库、获取匿名拉取令牌,并下载真实的镜像清单(manifest)和数据块(blob)。其中包括一个名为 baseten/baseten-app 的镜像。
到这一步,只报告一个暴露的镜像仓库然后收工本来很容易。但公司确实会有意发布镜像,而 Strix 从不想报告误报。真正重要的问题是:镜像被暴露会造成什么影响。
Strix 决定拉取一个镜像,看看里面有什么。以下是这次运行中该环节在 Strix 里的呈现(没错,智能体自己写的东西读起来像 AI,原因显而易见):
Harbor 暴露影响评估
思考
如果这些 Harbor 项目本不该公开,那这就是真实的暴露,而不是元数据噪音。关键在于整条链路:枚举公开项目,读取制品元数据,签发一个作用域为 repository:baseten/baseten-app:pull 的匿名 service/token,拉取清单,再通过带签名的存储重定向获取数据块。这就证明了匿名拉取权限确实存在。要让分诊人员认真对待,办法是从拉取到的镜像中证明一件事:专有代码、内部二进制文件、硬编码凭证,或内部主机名。
第一个有希望的发现是 baseten/baseten-app 中的一对 AWS 密钥。Strix 尝试了一次只读的 sts:GetCallerIdentity 调用,这种调用可以告诉你某个凭证属于哪个账户。返回的响应是 InvalidClientTokenId。
那把密钥已经失效,于是 Strix 继续找。
然后,一个真正有效的令牌
它拉取了各层,运行了 TruffleHog(向我们的开源安全朋友致敬!),并直接检查了镜像配置。然后它就出现了:一个典型的 GitHub 个人访问令牌,就躺在 history[].created_by 里。
我不是 Docker 运行时方面的专家,但幸运的是 Strix 是(得益于它几乎掌握了人类的全部知识)。所以它知道这个字段记录的是某个构建步骤是如何创建的。在这个案例中,它包含一条 RUN 命令,其中 GITHUB_TOKEN 的值被直接展开了进去。
Strix 用这个令牌向 GitHub 发了一个只读的 GET /user 请求,然后……瞧!200,账户名为 basetenbot。

请注意令牌是在哪里被发现的。我了解到,Docker 镜像除了文件系统层之外,还有一份配置,包含镜像的相关信息和构建历史。这份配置可以和镜像一起下载。如果构建历史里还留有令牌的另一份副本,那么清理凭证文件也无济于事。
而这个令牌在三年多之后依然有效。
好,那么 basetenbot 能做什么?

一个有效的令牌固然有意思,但显然权限才是关键。这个令牌完全可能没有任何权限,也就没有任何影响。于是 Strix 检查了该账户及其组织成员身份。GitHub 返回了 X-OAuth-Scopes: repo,该账户隶属于 basetenlabs。

随后它再次使用只读请求,检查了各个仓库的权限:
| 仓库 | 访问权限 |
|---|---|
basetenlabs/b*** |
admin: true, push: true |
basetenlabs/f*** |
admin: true, push: true |
basetenlabs/h*** |
admin: true, push: true |
basetenlabs/r*** |
私有,读写 |
basetenlabs/b*** |
私有,读写 |
basetenlabs/t*** |
私有,读写 |
basetenlabs/b*** |
私有,读写 |
把如此巨大的访问权限留在一个可公开下载的镜像里,实在离谱。
到这一步,我们已经掌握了足够的证据来报告,也确信这不是误报。我们没有克隆客户仓库,没有推送任何内容,也没有更改任何配置。我们就此打住,并立即撰写了披露邮件。
令牌是怎么跑到那里的?
构建历史带有时间戳。包含令牌的那一步运行于 2023 年 3 月 3 日。这是一个旧的构建凭证,而我们在 2026 年 7 月测试时,它仍然拥有全部这些权限。
背后的错误相当常见。某次构建需要从 GitHub 拉取私有依赖,于是有人把令牌作为构建参数传了进去。相关写法如下:
| 1 | ARG GITHUB_TOKEN |
| 2 | RUN GITHUB_TOKEN=${GITHUB_TOKEN} bash -c '\ |
| 3 | if [[ "${GITHUB_TOKEN}" != "" ]]; then \ |
| 4 | git config --global --add \ |
| 5 | url."https://${GITHUB_TOKEN}@github.com/".insteadOf "git@github.com:"; \ |
| 6 | fi' |
我能理解有人为什么会这么写。你需要一个私有依赖,传入令牌,Git 完成认证,构建就能跑通。但 Docker 会把这个构建参数记录在镜像的元数据和历史中。在这个案例里,它记录下的就是真实的令牌值。Docker 对此有明确的警告。
这种写法还有第二个问题:git config --global 会把带认证信息的 URL 写入 Git 的配置文件。即使你改变了令牌进入构建的方式,仍然需要避免把它保存进镜像。
解决办法是使用 BuildKit 的 secret mount,并采用不会持久化凭证的临时认证。然后同时检查镜像的各层和它的历史。还要吊销旧令牌!修改 Dockerfile 对已经被别人下载走的镜像毫无作用。
Strix 自主完成了什么
Baseten 有一支响应迅速的安全团队,而且已经在使用 AI 安全工具。尽管如此,我们发现时,这个来自 2023 年某次构建的令牌仍然对其产品仓库和部署仓库拥有管理员权限。
人们很容易只关注应用和源代码仓库,而忘掉一个旧的容器镜像。即便你扫描了镜像里的文件,也仍然需要检查它的构建历史。
我喜欢这次扫描的地方在于,Strix 一直顺着线索追查。它找到了一个镜像仓库,检查自己能否真的拉取镜像,测试一个凭证发现已经失效,又在构建历史中找到另一个凭证,并检查它能访问什么。
我们没有让它去找 Harbor,也没有给它任何关于令牌的提示。它在大约 25 分钟内自主完成了整个过程。
这正是我们开发 Strix 的原因。过去几周,AI 驱动的攻击变得极其可怕,我们相信,自我防御的唯一办法就是持续攻击自己,赶在坏人之前发现这些问题(因为问题永远会存在)。
披露
Baseten 处理得很好。时间线如下:
- 7 月 13 日晚 11:10:我报告了这个有效的
basetenbot令牌、公开的 Harbor 项目以及仓库权限。 - 7 月 14 日上午:Baseten 将 Harbor 项目设为私有。我指出令牌本身仍然有效。
- 7 月 14 日下午 4:34:Baseten Security 的 Anton 确认该问题为严重级别,并表示他们已将 Harbor 项目设为私有并轮换了令牌。他还请我们安全删除已拉取的镜像。
- 7 月 14 日下午 5:05:我们确认已删除,并把同一次扫描中发现的另外两个较低严重程度的问题发了过去。
- 7 月 17 日:Baseten 处理完了其余所有发现。
- 9 月:我们告知 Baseten,我们计划公开披露这一发现,并把本文草稿发给了他们。
为感谢我们发现这个严重漏洞,他们还给我们寄了一些 T 恤和卫衣。
去检查一下你的旧镜像
如果你运行容器并使用 GitHub,这件事值得在你自己的基础设施里查一查:
- 看看别人在不登录的情况下能拉取到什么,包括旧标签,以及你已经很久没想起过的项目。
- 用
docker history --no-trunc读取构建历史,或者检查配置 blob 的history[].created_by字段。各层也要检查。 - 把密钥从构建参数里拿出来。使用 secret mount,并确保使用这些密钥的命令不会把它们写回镜像。
- 检查你的构建令牌实际上能做什么。拉取一个依赖只需要对该依赖的读权限。如果给这个令牌授予产品仓库和部署仓库的管理员权限,一旦泄露后果会严重得多。请限制权限,并设置过期时间。
另外,请对你自己的系统运行类似 Strix 的工具。这整次扫描的起因,是我们想使用一家推理服务商。我们给了它一个域名,换回了一个严重漏洞,Baseten 第二天早上就能着手处理。
AI 攻击者同样可以沿着这些路径走。如果一个智能体能在 25 分钟内从旧镜像里找到一个有效的管理员令牌,你最好让自己的工具抢先找到它。
