flawopen.com/Incidents/rubygems-cdn-cache-api-key-leak-july-2026
RubyGems:CDN缓存导致API密钥泄露
gzip/CDN缓存缺陷泄露了RubyGems旧密钥。本文说明修复与恢复措施;软件包活动是否涉及AI代理仍有争议,归因尚未证实。
想象一个服务窗口为每位通过核验的客户生成一把主钥匙,却因为包装设备改变了信封标签,把副本放进了公共托盘。下一位在同一窗口办理的人可能无需出示证件,就拿到上一位客户的钥匙。修复要求共享缓存不要存储此类私有响应,并按授权信息区分响应。
核心概念与专有名词
Legacy API key- 一种长期有效、拥有广泛 gem 管理权限的 RubyGems 凭据。公告称它可发布新版本、撤回版本、添加所有者并修改 webhook 或可信发布者设置;它不能登录账户,也不能改写已发布的版本。
Shared cache / POP- CDN 的边缘节点会保存响应以供后续请求复用。如果包含凭据的认证响应没有私有缓存策略,也没有按授权信息区分的缓存键,其他调用者就可能拿到该响应。
Compression middleware- RubyGems 表示,Accept-Encoding: gzip 会触发 Rack 中间件交互,导致响应缺少 private 指令和 Vary: Authorization。普通的不带 gzip 请求没有呈现相同的响应头。
CWE-200 and CWE-524- GitHub Security Advisory 将
CWE-200标为敏感信息泄露,将CWE-524标为使用包含敏感信息的缓存。两者分别描述信息泄露和缓存边界;该问题没有分配 CVE。 RubyGems package-flood attribution- RubyGems 确认 2026 年 5 月存在滥用行为并发布了恶意软件包,但称无法判断这些软件包是否由 AI 代理创建或发布。 此活动必须与缓存缺陷区分:来源没有证明它导致了密钥泄露。
框架映射
事件演进时间线
RubyGems 3.2.0 将常规 gem signin 流程改为创建受限范围的密钥,但服务器仍保留旧 GET 路由。
Truffle Security 向 RubyGems 报告了缓存问题。
RubyGems 部署响应头修复,并清除了 Fastly 中受影响的缓存对象。
RubyGems 发布 GHSA-9j48-x3c3-mrp2,并称已撤销所有旧密钥。GHSA 评分为 7.3 High;RubyGems 报告综合环境评分为 7.2 High。
RubyGems 单独说明了 5 月的软件包发布活动,并称无法将其归因于 AI 代理。
攻击执行流程分解
旧接口签发凭据
对 /api/v1/api_key 发起经过认证的 GET 请求会创建并返回权限广泛的旧版 API 密钥。RubyGems 称 3.2.0 之前的 gem 客户端使用此路由;新客户端改用受限范围密钥后,服务器仍保留了旧接口。
gzip 改变响应头处理路径
客户端请求 gzip。RubyGems 将缓存行为归因于 Rack::Deflater 与 Rack::ETag 的交互:压缩后的响应只带有普通的 Cache-Control: no-cache,缺少 private 和 Vary: Authorization。
CDN复用认证响应
在受影响的 Fastly 配置中,响应可能在边缘 POP 缓存长达一小时。缓存键没有按 Authorization 区分调用者,因此边缘节点可能直接返回先前的响应,而不回源验证下一位客户。
另一位调用者收到密钥
通过同一 POP、请求相同缓存变体的调用者可能收到前一位用户的密钥。RubyGems 表示,这可能在用户登录时意外发生;也描述了未认证地读取缓存响应的情况。现有日志未发现恶意使用,但只覆盖有限时间段。
密钥权限限制影响范围
泄露的旧密钥可用于发布新 gem 版本、撤回版本、更改所有者,以及修改 webhook 或可信发布者。已发布的版本无法被改写。RubyGems 表示,同时要求界面和 API MFA 可阻止仅凭密钥执行发布、撤回和所有者变更。
修复阻止共享复用并撤销暴露密钥
补丁加入 Cache-Control: private, no-store、Surrogate-Control: max-age=0 和 Vary: Authorization。RubyGems 清理了 Fastly 对象、撤销旧密钥并退役 GET 登录路由。建议使用受限密钥、启用 API MFA,并在 CI 中使用可信发布。
源代码对比:漏洞与安全实现
未修复控制器片段依据 RubyGems 公开修复中被删除的代码重建;不安全缓存头来自 Rack 中间件交互,而不是该控制器动作显式设置了公共缓存策略。加固后的 callback 和头部值与提交 d3d11c0 一致。private, no-store 禁止共享存储,Surrogate-Control 禁用代理缓存,Vary: Authorization 则区分仍会存储响应的缓存。此处是源码分析,不是请求操作指南,也不是可独立使用的补丁;应采用供应商完整修复并清理旧缓存项。
class Api::V1::ApiKeysController < Api::BaseController
include ApiKeyable
# 修复前的控制器没有 deny_shared_cache 回调。
def show
authenticate_or_request_with_http_basic do |username, password|
user = User.authenticate(username.strip, password)
check_mfa(user) do
key = generate_unique_rubygems_key
api_key = user.api_keys.build(
legacy_key_defaults.merge(hashed_key: hashed_key(key))
)
save_and_respond(api_key, key)
end
end
end
end
class Api::V1::ApiKeysController < Api::BaseController
include ApiKeyable
before_action :deny_shared_cache
def show
authenticate_or_request_with_http_basic do |username, password|
user = User.authenticate(username.strip, password)
check_mfa(user) do
key = generate_unique_rubygems_key
api_key = user.api_keys.build(
legacy_key_defaults.merge(hashed_key: hashed_key(key))
)
save_and_respond(api_key, key)
end
end
end
end
# 修复时添加到 Api::BaseController 中。
def deny_shared_cache
response.headers["Cache-Control"] = "private, no-store"
response.headers["Surrogate-Control"] = "max-age=0"
response.headers["Vary"] = [response.headers["Vary"], "Authorization"].compact_blank.join(", ")
end
工程与系统安全加固清单
- ✓如果使用过 RubyGems.org 旧版凭据,请更换:RubyGems 已撤销所有旧密钥。审计版本、异常撤回、所有者、可信发布者和 webhook;不要误以为现有版本被改写。
- ✓使用仅对所需 gem 和操作授权的 API 密钥,为界面和 API 操作启用 MFA,并在 CI 中优先使用可信发布/OIDC,避免长期有效的密钥。验证每个工作流只能发布预期的 gem。
- ✓对于 CDN 后的认证响应或机密,在源站设置 Cache-Control: private, no-store,禁用代理缓存,并将所有授权维度纳入 Vary/缓存键策略。通过真实 CDN 路径分别测试启用和关闭压缩的情况。
- ✓增加集成测试:使用 Accept-Encoding: gzip 请求凭据接口,并断言 private、no-store、Surrogate-Control: max-
age=0和 Vary: Authorization。还要验证第二个身份无法从共享边缘缓存中拿到第一个身份的响应。 - ✓修复缓存泄露时,先清除缓存对象,再签发替代密钥;将历史日志视为有范围限制的证据,而非“没有泄露”的证明,并记录可检查的时间范围。
- ✓准确说明归因:RubyGems未确认2026年5月的软件包活动由AI创作,也未发现API密钥窃取尝试成功的证据。
参考来源
- RubyGems — Security advisory: Possible leak of legacy API keys via improper cache configuration
- RubyGems.org — GHSA-9j48-x3c3-mrp2
- RubyGems.org — remediation commit d3d11c0
- Truffle Security — Cache vulnerability in RubyGems
- RubyGems — API key scopes
- RubyGems — May spam-publishing campaign update
- CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor
- CWE-524 — Use of Cache Containing Sensitive Information