Red Hat Quay:通过密钥轮换接管镜像仓库(CVE-2026-123710)
在修复分支版本之前,Red Hat Quay 允许未认证调用者安装签名密钥,并为任意仓库伪造令牌。
TL;DR
- Red Hat Quay 的密钥服务器会把匿名仓库令牌当作密钥轮换凭据。任何能访问
/keys的人都可以安装签名密钥,并为所有仓库伪造令牌。该漏洞编号为 CVE-2026-123710,于 2026 年 11 月 11 日公开。 - 第二个严重漏洞 CVE-2026-110697 允许能创建命名空间的用户,通过未转义的 SQL
LIKE模式读取其他租户的机器人账户令牌。两者都影响各自分支修复版本之前的自托管 Quay。 - 两个漏洞的 CVSS v3.1 评级均为严重:CVE-2026-123710 为 9.8,CVE-2026-110697 为 9.9。截至 2026 年 11 月 12 日,没有已知利用,两者都不在 CISA 的 KEV 目录中。Red Hat 表示已修复 Quay.io。
- 请升级到所属分支的修复版本(3.9.28、3.10.28、3.12.24、3.14.10、3.15.9、3.16.7、3.17.7 或 3.18.3)。如果无法升级,请在代理层阻止
/keys,并检查已批准的服务密钥。
我受影响吗?
如果适用于您所检查漏洞的条件全部成立,您就受影响:
- 您运行自托管的 Red Hat Quay 3 或 Quay 5,且其版本早于所属分支的修复版本。
- 对于 CVE-2026-123710,
/keys端点可从不受信任的客户端访问。Quay 3.18.2 默认启用匿名访问(FEATURE_ANONYMOUS_ACCESS = True),而密钥服务器接受的正是匿名令牌。 - 对于 CVE-2026-110697,部署是多租户的,攻击者拥有可创建组织或用户命名空间的账户。
密钥接管漏洞不需要账户。机器人令牌漏洞需要攻击者能够命名的命名空间。单租户仓库仍会受到第一个漏洞影响,因为伪造令牌可以访问所有仓库。
| 分支 | 受影响版本 | 修复版本 |
|---|---|---|
| 3.9 | 3.9.28 之前的所有版本 | 3.9.28 |
| 3.10 | 3.10.28 之前的所有版本 | 3.10.28 |
| 3.12 | 3.12.24 之前的所有版本 | 3.12.24 |
| 3.14 | 3.14.10 之前的所有版本 | 3.14.10 |
| 3.15 | 3.15.9 之前的所有版本 | 3.15.9 |
| 3.16 | 3.16.7 之前的所有版本 | 3.16.7 |
| 3.17 | 3.17.7 之前的所有版本 | 3.17.7 |
| 3.18 | 3.18.3 之前的所有版本 | 3.18.3 |
Red Hat 在 CVE 页面中也将 Red Hat Quay 5 列为受影响。这些页面没有单独列出 Quay 5 的构建版本,因此 Quay 5 客户在安排升级前应查看其订阅的勘误。
在 OpenShift 部署中,运行中的 Quay 镜像标签显示版本。该标签由 Operator 设置,因此应读取 Deployment,而不是 Operator 版本:
oc get deployment -A -l quay-component=quay-app \
-o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.spec.template.spec.containers[0].image}{"\n"}{end}'
在 Podman 部署中,请检查运行中的容器:
podman inspect quay --format '{{.Config.Image}}'
该标签就是 Quay 版本。没有可读标签的摘要同样对应一个版本,skopeo inspect docker://<image> 会打印该摘要的标签。
如何修复?
Red Hat 在 CVE 页面中列出每个分支的修复版本。请安装对应镜像,或将 Operator 订阅切换到包含修复的频道,然后确认 Quay Pod 已在新镜像上重启。
- 记录每个 Quay 部署的当前镜像:
oc get deployment -A -l quay-component=quay-app \ -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.spec.template.spec.containers[0].image}{"\n"}{end}' - 通过 Operator 更新 Quay 仓库,或拉取所属分支的修复标签并重新部署。
- 确认新 Pod 使用修复后的镜像:
oc get pods -A -l quay-component=quay-app \ -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{" "}{.status.containerStatuses[0].image}{"\n"}{end}' - 对每个 Quay 实例重复操作,包括与生产共享凭据的灾难恢复和预发布仓库。
如果您自行构建开源项目,修复位于 master 分支的提交 094cfb024f01,于 2026-11-11 合并。上游最新标签仍是 v3.18.2,因此尚无带标签的上游版本包含修复。Red Hat 的修复版本是受支持的路径。
如果无法立即更新,请用以下措施降低暴露。它们都不能替代补丁。
- 在代理或负载均衡器上阻止
/keys,只允许受信任的内部组件访问。这会关闭密钥接管路径。 - 列出已批准的服务密钥,删除任何不认识的密钥,尤其是没有到期时间的密钥。只要
/keys仍可访问,已删除的密钥就可能被重新安装,因此请在阻止生效后再执行此操作。 - 如果部署不需要匿名访问,请将其关闭。这会移除未认证令牌来源,但拥有任意账户的攻击者仍可能访问密钥服务器。
- 对于 CVE-2026-110697,限制可创建组织的人员,并轮换名称包含下划线或与其他命名空间冲突的命名空间中的机器人令牌。
- 检查到期时间为空的
service_key_rotate和service_key_delete审计记录,以及不符合预期自动化的机器人令牌使用。
根本原因是什么?
这两个漏洞是同一产品中的两个独立错误。它们都把受信任的值变成授权决定,却没有检查该值是为何目的签发的。
第一个漏洞在密钥服务器中。_validate_jwt 验证签名、受众和签发者,然后丢弃负载:
def _validate_jwt(encoded_jwt, jwk, service):
public_key = jwtutil.jwk_dict_to_public_key(jwk)
try:
jwtutil.decode(
encoded_jwt, public_key, algorithms=["RS256"], audience=JWT_AUDIENCE, issuer=service
)
except jwtutil.InvalidTokenError:
logger.exception("JWT validation failure")
abort(400)
由 /v2/auth 签发的仓库令牌满足这三项检查。该端点从客户端提供的 service 参数获取受众,并用实例密钥签名令牌:
audience_param = request.args.get("service")
token = generate_bearer_token(
audience_param, subject, context, access, TOKEN_VALIDITY_LIFETIME_S, instance_keys
)
密钥服务器随后将该令牌视为调用者可以轮换服务密钥的证明。它从不检查轮换声明,也不检查该令牌不是仓库令牌。put_service_key 接受没有到期时间的新密钥,replace_service_key 把签名者的批准复制到新密钥并删除签名者:
def replace_service_key(old_kid, kid, jwk, metadata, expiration_date):
try:
with db_transaction():
key = db_for_update(ServiceKey.select().where(ServiceKey.kid == old_kid)).get()
key.metadata.update(metadata)
ServiceKey.create(
name=key.name,
kid=kid,
service=key.service,
jwk=jwk,
metadata=key.metadata,
expiration_date=expiration_date,
rotation_duration=key.rotation_duration,
approval=key.approval,
)
key.delete_instance()
第二个漏洞在机器人列表中。_list_entity_robots 用命名空间名称构造 LIKE 模式,却没有转义通配符:
.where(User.robot == True, User.username ** (entity_name + "+%"))
Peewee 会把 ** 转换为 LIKE 比较。在 SQL 中,_ 匹配任意单个字符,% 匹配任意字符串。因此名为 ab_c 的命名空间会匹配 abxc 和 ab-c 中的机器人。同一查询连接了 RobotAccountToken,所以匹配行带有解密后的令牌。
如何被利用?
对于 CVE-2026-123710,攻击者只需要网络能访问 /keys,不需要账户:
- 从
/v2/auth请求匿名仓库令牌,将service参数设为 Quay 主机名,使受众匹配。 - 生成 RSA 密钥对,并为公钥构造 JWK。
- 将匿名令牌作为 Bearer 凭据,向
/keys/services/quay/keys/<kid>发送PUT,正文为攻击者的 JWK,且不带expiration参数。 - 密钥服务器验证令牌签名、受众和签发者。它不检查令牌是否为密钥轮换而签发。
replace_service_key将实例密钥的批准状态复制到攻击者密钥,保存为无到期时间,并删除实例密钥。- 攻击者用私钥签名仓库令牌。Quay 会为任意仓库接受这些令牌,包括推送和覆盖。
对于 CVE-2026-110697,攻击者需要一个能创建命名空间的账户:
- 创建一个组织或用户命名空间,其名称与受害者命名空间只差一个字符,并在该位置使用下划线。
- 请求新命名空间的机器人列表。Quay 用该名称构造
LIKE模式,下划线匹配受害者的字符。 - 响应包含受害者的机器人账户及其解密令牌。
- 使用窃取的令牌拉取或推送受害者镜像。重命名或删除攻击者的命名空间也可能重命名或删除匹配的受害者机器人。
攻击如何展开?
一个漏洞把匿名仓库令牌变成受信任的签名密钥,另一个把命名空间名称变成通配符,读取另一租户的机器人令牌。
仓库令牌"] --> B["/v2/auth 用实例密钥
签名"] B --> C["密钥服务器验证签名
但不检查轮换声明"] C --> D["攻击者安装无到期
时间的 JWK"] D --> E["批准被复制到
攻击者密钥"] E --> F["伪造令牌被所有
仓库接受"] G["包含下划线的
命名空间名称"] --> H["SQL LIKE 匹配另一
租户的机器人名称"] H --> I["跨租户读取
机器人令牌"]
攻击者能做什么?
完成密钥接管的攻击者可以读写 Quay 提供的所有仓库,包括私有镜像。伪造令牌是普通仓库令牌,因此仓库没有理由拒绝。攻击者可以替换标签、向 CI/CD 流水线注入镜像,或拉取私有镜像进行检查。
密钥接管还会删除合法的实例密钥。除非安装修复版本并删除恶意密钥,否则攻击者密钥会保持批准状态,真实运维人员之后的轮换可能被撤销。
机器人令牌漏洞让攻击者获得被盗机器人的权限。具有推送权限的机器人可以覆盖受害者命名空间中的镜像。具有管理员权限的机器人可以更改仓库设置。攻击者还可以通过自己的命名空间重命名或删除受害者的机器人。
两个漏洞都不需要用户交互。CVE-2026-123710 需要 /keys 可达且默认匿名访问开启。CVE-2026-110697 需要多租户部署中可创建命名空间的账户。
为什么修复有效?
修复在一个提交 094cfb024f01 中关闭了两条路径。它为密钥服务器添加轮换声明,固定仓库受众,要求新密钥有到期时间,并转义机器人 LIKE 模式。
密钥服务器现在返回解码后的负载。它要求 key_rotation 声明与要安装的密钥匹配:
+ if "access" in payload or "context" in payload:
+ abort(403)
+
+ return payload
+
+
+def _validate_rotation_claim(payload, kid):
+ if payload.get(KEY_ROTATION_CLAIM) != kid:
+ abort(403)
- _validate_jwt(encoded_jwt, signer_jwk, service)
+ payload = _validate_jwt(encoded_jwt, signer_jwk, service)
+ _validate_rotation_claim(payload, kid)
+
+ if signer_key.expiration_date is not None:
+ expiration_date = min(expiration_date, signer_key.expiration_date)
仓库令牌带有 access 和 context 声明,因此会被拒绝。轮换令牌必须指定新密钥,因此为其他目的签发的令牌无法安装密钥。新密钥还必须带有未来的到期时间,其生命周期受签名者到期时间限制:
- if expiration_date is not None:
- try:
- expiration_date = datetime.utcfromtimestamp(float(expiration_date))
- except ValueError:
- logger.exception("Error parsing expiration date on key")
- abort(400)
+ if expiration_date is None:
+ abort(400)
+
+ try:
+ expiration_date = datetime.utcfromtimestamp(float(expiration_date))
+ except (TypeError, ValueError, OverflowError):
+ logger.exception("Error parsing expiration date on key")
+ abort(400)
+
+ if expiration_date <= datetime.utcnow():
+ abort(400)
受众被固定为配置的主机名,因此攻击者无法让 /v2/auth 签发面向其他服务的令牌:
token = generate_bearer_token(
- audience_param, subject, context, access, TOKEN_VALIDITY_LIFETIME_S, instance_keys
+ app.config["SERVER_HOSTNAME"],
+ subject,
+ context,
+ access,
+ TOKEN_VALIDITY_LIFETIME_S,
+ instance_keys,
)
机器人列表现在使用 prefix_search,它在查询前转义 \、% 和 _:
- .where(User.robot == True, User.username ** (entity_name + "+%"))
+ .where(User.robot == True, prefix_search(User.username, entity_name + "+"))
修复还将密钥替换和删除限制在指定服务内,因此一个服务的密钥无法替换或删除另一个服务的密钥。提交中新增的测试复现了下划线冲突、用户名更改和命名空间删除路径。
如何检查是否已遭入侵?
密钥服务器会把服务密钥操作写入审计日志。请查找运维人员未执行的轮换和删除,以及没有到期时间的密钥:
- 到期时间为空的
service_key_rotate和service_key_delete事件。 - 不认识的密钥的
service_key_create事件。 - 机器人令牌使用与自动化不匹配的仓库拉取或推送。
超级用户 API 列出已批准的服务密钥。请检查列表和密钥元数据,包括到期时间:
curl -s -H "Authorization: Bearer <token>" https://<quay-host>/api/v1/superuser/keys
对于机器人令牌漏洞,请列出名称包含下划线的命名空间中的机器人,并与预期自动化比较:
curl -s -H "Authorization: Bearer <token>" https://<quay-host>/api/v1/organization/<org>/robots
两个漏洞都没有公开的网络指标、文件名或请求签名。成功的密钥接管会在数据库中留下已批准的密钥,该密钥是最有力的证据。失败的尝试可能只在 /keys 留下 400 或 403,不留下审计记录。
时间线
- 2026-10-14:Red Hat 为机器人列表
LIKE注入创建第一个缺陷记录(CVE-2026-110697)。 - 2026-11-06:Red Hat 为未认证密钥轮换创建第二个缺陷记录(CVE-2026-123710)。
- 2026-11-11:Red Hat 于 15:02 和 17:06 UTC 发布两个 CVE 记录,并于 19:43 UTC 合并修复提交。Red Hat 表示 Quay.io 已修复,并列出了每个分支的修复版本。
两份报告在 28 天内得到修复:从第一份到第二份为 23 天,从第二份到修复为 5 天。
常见问题
Quay.io 受影响吗?
截至 2026 年 11 月 11 日,托管服务不受影响。Red Hat 表示已修复 Quay.io。漏洞影响未应用修复版本的自托管 Red Hat Quay。
密钥接管需要匿名访问吗?
不需要,但它使攻击者无需账户。Quay 3.18.2 中 FEATURE_ANONYMOUS_ACCESS 默认为 True,/v2/auth 会签发密钥服务器接受的令牌。即使关闭匿名访问,拥有有效账户的攻击者仍可能获取仓库令牌并尝试同一请求。
拥有被盗机器人令牌的攻击者能接管仓库吗?
只能在该机器人的权限范围内。具有推送权限的机器人可以覆盖其命名空间中的镜像。密钥接管不同,因为它为所有仓库伪造令牌,包括攻击者目前无法命名的仓库。
应该安装哪个版本?
请安装所属分支的修复版本:3.9.28、3.10.28、3.12.24、3.14.10、3.15.9、3.16.7、3.17.7 或 3.18.3。如果从源码构建,请应用提交 094cfb024f01。上游最新标签仍是 v3.18.2,因此带标签的上游版本尚未包含修复。
需要轮换机器人令牌吗?
如果部署中的某个命名空间名称包含下划线,且另一个命名空间只在该字符上不同,则需要。请轮换两个命名空间中的机器人,并检查它们最近的拉取和推送。被读取的令牌与未被使用的令牌无法区分。
无法更新时有变通方法吗?
在代理层阻止 /keys,并删除不认识的已批准密钥。限制组织创建,并轮换冲突的机器人令牌。这些措施降低暴露,但在安装修复版本之前,密钥服务器仍会信任匿名仓库令牌。
参考文献
- CVE-2026-123710 的 Red Hat CVE 页面,https://access.redhat.com/security/cve/CVE-2026-123710(2026 年 11 月 12 日检查)。
- CVE-2026-110697 的 Red Hat CVE 页面,https://access.redhat.com/security/cve/CVE-2026-110697(2026 年 11 月 12 日检查)。
- Red Hat Bugzilla 2568072,密钥轮换漏洞,https://bugzilla.redhat.com/show_bug.cgi?id=2568072(2026 年 11 月 12 日检查)。
- Red Hat Bugzilla 2552237,机器人列表漏洞,https://bugzilla.redhat.com/show_bug.cgi?id=2552237(2026 年 11 月 12 日检查)。
- Quay 修复提交 094cfb024f01,https://github.com/quay/quay/commit/094cfb024f01(2026 年 11 月 12 日检查)。
- Quay v3.18.2 源码,
endpoints/keyserver/__init__.py,https://github.com/quay/quay/blob/v3.18.2/endpoints/keyserver/init.py(2026 年 11 月 12 日检查)。 - Quay v3.18.2 源码,
data/model/user.py,https://github.com/quay/quay/blob/v3.18.2/data/model/user.py(2026 年 11 月 12 日检查)。 - Quay v3.18.2 源码,
endpoints/v2/v2auth.py,https://github.com/quay/quay/blob/v3.18.2/endpoints/v2/v2auth.py(2026 年 11 月 12 日检查)。 - Quay v3.18.2 源码,
data/model/service_keys.py,https://github.com/quay/quay/blob/v3.18.2/data/model/service_keys.py(2026 年 11 月 12 日检查)。 - CVE-2026-123710 记录,https://www.cve.org/CVERecord?id=CVE-2026-123710(2026 年 11 月 12 日检查)。
- CVE-2026-110697 记录,https://www.cve.org/CVERecord?id=CVE-2026-110697(2026 年 11 月 12 日检查)。
- CISA 已知被利用漏洞目录,https://www.cisa.gov/known-exploited-vulnerabilities-catalog(2026 年 11 月 12 日检查)。