SN-2026-022:双活复制标签代理(Tagging Proxy)未认证删除与远端标签策略绕过
发布状态: 主干已修复(14ecb222e、67a90dfef、293ab78b4),将随 20261109 之后的下一版 Server 发布
安全编号: SN-2026-022(安全公告台账,#337)
严重程度: 严重(Critical)
影响范围: SILO Server(RELEASE.2026-11-09T00-00-00Z 及更早版本)与自 2024-01-12(b2b26d9c9)以来的上游原版 MinIO
不受影响组件: silo-console、mcli、silo-pkg 与 kes
在启用双活(Active-Active)桶复制或站点复制的拓扑中,某个对象版本可能已写入远端复制对端,但尚未同步至当前接收客户端请求的本地集群。为保持跨站标签读写透明一致,GetObjectTaggingHandler、PutObjectTaggingHandler 与 DeleteObjectTaggingHandler(cmd/object-handlers.go)在本地未找到目标对象时,会通过 proxyTaggingToRepTarget(cmd/bucket-replication.go)使用服务端持有的高权限复制目标凭据向对端站点代理请求。
继承自上游 MinIO 的这一代理实现存在两处安全缺陷:DeleteObjectTaggingHandler 在执行调用方身份认证与权限检查之前就查询本地对象并向远端发起代理删除;同时三个标签接口在代理本地缺失对象时均未读取远端副本上的既有标签,导致基于 s3:ExistingObjectTag/<key> 的 IAM 策略条件失效。
摘要
- 问题本质:
- 未认证远端标签抹除与私有桶对象探测:
DeleteObjectTaggingHandler将getObjectInfo与proxyTaggingToRepTarget放在了checkRequestAuthType之前。当目标对象仅存在于双活对端站点时,完全未携带任何签名的匿名请求DELETE /<bucket>/<key>?tagging会驱动服务端使用内部复制凭据直接清空远端对象标签并返回204 No Content;即便未配置复制,匿名请求也可在无s3:ListBucket权限的私有桶上通过404 NoSuchKey与403 AccessDenied的响应差异探测任意对象是否存在。 - 跨站点
s3:ExistingObjectTag条件策略绕过: 当对象在本地不存在时,GetObjectTagging、PutObjectTagging与DeleteObjectTagging仅按空标签集("")评估本地 IAM 策略便直接向双活对端转发操作——既使受s3:ExistingObjectTag/*显式Deny约束(例如禁止修改带classification=restricted标签对象)的调用方可通过访问缺副本站点绕过拦截,也使依赖标签匹配才授予Allow的合法请求被误判为403 AccessDenied。
- 未认证远端标签抹除与私有桶对象探测:
- 上游溯源: 两处问题均由上游 MinIO 于
2024-01-12合入的提交b2b26d9c9(minio/minio#18649,“support proxying of tagging requests in replication”)引入,影响 2024 年 1 月之后的所有上游 MinIO 版本及 SILO Server20261109与更早版本。 - 修复方式: 提交
14ecb222e、67a90dfef与293ab78b4在入口处无条件执行调用方认证,以authorizeRequestWithUnreadTags拦截无权限调用方的跨站探测,在返回或修改前读取并逐一校验所有已存在双活副本的实际标签,同时容忍复制延迟导致的单目标缺失并对不可读目标执行 Fail-Closed 拒绝。
一、上游溯源与根因分析
DeleteObjectTaggingHandler 中的认证前代理执行
在 PutObjectTaggingHandler 与 GetObjectTaggingHandler 中,上游代码先执行认证再访问存储层;但在 DeleteObjectTaggingHandler 中,上游提交 b2b26d9c9 将本地元数据读取与双活代理逻辑直接插入到了 checkRequestAuthType 之前:
这一顺序直接造成两重后果:
- 匿名清空远端副本标签: 只要请求命中的站点本地尚未存有
(bucket, object)(返回ObjectNotFound或VersionNotFound),且匹配的复制规则选中了允许代理并持有对象的目标,proxyTaggingToRepTarget就会用服务端预先配置的复制凭据调用clnt.RemoveObjectTagging,在未验证来访请求任何签名的情况下抹除对端站点上的对象标签并返回204 No Content。 - 私有桶键名存在性泄露: 即使在未开启复制的普通私有桶上,匿名请求对不存在的键返回
404 NoSuchKey,对本地已存在的键则进入checkRequestAuthType返回403 AccessDenied,从而无需s3:ListBucket即可枚举桶内对象路径。
跨站点代理缺失 s3:ExistingObjectTag 条件评估
S3 与 SILO 支持通过 s3:ExistingObjectTag/<key> 条件键在对象标签上实施基于属性的访问控制(ABAC):
GetObjectTagging/GetObjectVersionTaggingPutObjectTagging/PutObjectVersionTagging(需同时校验证书已有标签s3:ExistingObjectTag/<key>与请求新标签s3:RequestObjectTag/<key>)DeleteObjectTagging/DeleteObjectVersionTagging
当对象存在于本地时,处理器会从 xl.meta 读出 oi.UserTags 并调用 authorizeRequestWithExistingTags。但当对象在本地缺失时,b2b26d9c9 在未查询远端标签的前提下仅用空标签 "" 做了一次本地检查便向对端发起代理:
- 若策略通过
Deny禁止调用方修改带有s3:ExistingObjectTag/tier == "prod"的对象,调用方只要将请求发往尚未同步到该对象的双活站点,本地就会按空标签放行,并由高权限复制账号代为覆盖或删除对端标签。 - 反之,若策略仅在
s3:ExistingObjectTag/team == "analytics"时授予Allow,客户端请求一旦落到缺副本站点就会被误拒为403 AccessDenied。 - 在三个及以上站点的多目标双活拓扑中,不同远端目标上的标签可能处于异步传播中间态。若只校验第一个响应的对端,或在完成全部存活副本鉴权前就开始下发写操作,仍会破坏跨副本标签策略的一致性边界。
二、影响范围
| 部署形态 | 实际受影响面 |
|---|---|
| 双活桶复制或站点复制集群 | 匿名请求可删除本站点缺失但对端存在的对象标签(DELETE ?tagging);已认证受限主体可在 GET/PUT/DELETE ?tagging 上绕过针对 s3:ExistingObjectTag 的 Deny 规则,而基于标签的 Allow 规则会被误拒。 |
| 单向或异步复制 | replicateSync=false 本身不会禁用代理;是否选中目标取决于匹配的复制规则和代理目标状态。不能把单向或异步模式当作免受影响的边界。 |
| 未启用复制的独立集群 | 未认证或无权限调用方可通过 DELETE ?tagging 探测私有桶内对象是否存在(无 s3:ListBucket 时仍返回 404 而非 403);本地已有对象的标签鉴权不受影响。 |
三、修复方案:设计思考与充分必要性
提交 14ecb222e、67a90dfef 与 293ab78b4 将 GetObjectTagging、PutObjectTagging 与 DeleteObjectTagging 统一收敛到同一套两阶段鉴权代理模型:
- 无条件前置身份认证: 三个处理器均在访问本地对象层或复制目标前先调用
authenticateRequest,彻底封堵匿名流量进入后续链路(除非桶策略显式允许匿名访问)。 - 跨站探测前置门禁(
authorizeRequestWithUnreadTags): 当本地返回ObjectNotFound/VersionNotFound时,服务端不会对任意已登录用户无条件发起跨站 RPC,而是先通过authorizeRequestWithUnreadTags判定“该调用方是否存在至少一种标签组合使其可能获准执行本操作”。若调用方根本没有任何匹配的Allow语句(或命中了与标签无关的Deny),本地直接拒绝并按s3:ListBucket权限决定返回404还是403;若本地存储发生可重试故障(如503 SlowDownRead),已获授权调用方将收到真实存储错误而非误报AccessDenied。 - 对入选目标进行两阶段读取与鉴权(
readTaggingProxyTargets):- 第一阶段(读取并授权每份入选的现存副本):
getProxyTargets按匹配的复制规则选择目标;readTaggingProxyTargets先跳过无客户端、已标记离线或禁用代理的目标,再发起GetObjectTagging。下列错误处理针对实际入选目标:- 若某个对端因复制延迟返回
NoSuchKey或NoSuchVersion,跳过该缺失目标继续检查其余对端;若所有对端均不存在该对象,则按标准缺失对象规则返回404或403。 - 若任一对端返回网络超时、认证失败或
5xx存储错误,立即 Fail-Closed 终止请求,绝不在无法确认远端是否存在受保护标签的情况下放行读写。 - 对每一个实际持有该对象版本的对端,使用其真实标签调用
authorizeRequestWithExistingTags(PutObjectTagging同时校验请求携带的新标签RequestObjectTag)。只要任一存活副本未通过策略评估,整次请求即返回403 AccessDenied。
- 若某个对端因复制延迟返回
- 第二阶段(仅向已核验目标下发变更):
PutObjectTagging与DeleteObjectTagging只复用第一阶段已完成鉴权的客户端句柄。至少一个实际尝试的写入成功时返回成功;所有实际写入都失败才返回错误。被跳过的目标不能算作成功,写入也不是跨目标原子事务。
- 第一阶段(读取并授权每份入选的现存副本):
为什么该设计既必要又充分
- 必要性: 对抗测试(
cmd/tagging-proxy-authz_test.go)逐项验证了缺少任一环节的后果:- 若缺少前置认证与
authorizeRequestWithUnreadTags门禁,无权限账号仍可触发跨站扇出读取或探测键名; - 若只抽检单个远端目标,在多站点标签尚未收敛时会漏掉另一站点上的
Deny标签;反之若机械要求“所有远端站点都必须持有副本”,正常的异步复制延迟就会导致双活标签操作大面积报404; - 若对读取失败的远端目标降级放行(Fail-Open),故障窗口内就会绕过基于标签的显式
Deny隔离。
- 若缺少前置认证与
- 充分性:
GET、PUT、DELETE三条标签路径共用readTaggingProxyTargets(cmd/bucket-replication.go),在单目标与多目标双活拓扑下统一保证了身份认证、ExistingObjectTag/RequestObjectTag条件求值、s3:ListBucket错误掩码以及远端故障原子拦截。
四、利弊权衡、潜在代价与边界
| 权衡维度 | 收益与代价说明 |
|---|---|
本地缺副本时的额外远端 GET 往返 |
本地已有对象不需要这些代理读取。当对象在本地缺失时,代理会先读取每个符合筛选条件的入选目标的标签,再返回或修改远端标签。 |
| 对端站点故障时的 Fail-Closed 可用性取舍 | 入选目标的标签读取失败时,代理请求会在变更前终止。筛选时已标记离线或禁用代理的目标会被跳过,因此并不保证检查每个已配置对端。 |
| 跨广域网无分布式锁的并发窗口 | 双活 S3 复制不在广域网站点间持有分布式互斥锁。第一阶段校验的是紧邻写入前的远端标签快照;跨站点同时并发修改同一对象标签时,最终状态仍按既有的复制标签时间戳(TaggingTimestamp)收敛。 |
升级指引与缓解建议
- 全站点协同升级 SILO Server: 请将双活复制拓扑中的所有节点升级至包含
14ecb222e、67a90dfef与293ab78b4的20261109后续版本。任意未升级的站点若暴露在客户端流量下,仍可能使用其复制凭据代发未认证的DELETE ?tagging。 - 升级前网关层临时缓解: 若短期内无法完成升级且 S3 端口面向不可信网络开放,可在反向代理或负载均衡层拦截同时不含
Authorization请求头与X-Amz-Signature查询参数的DELETE ...?tagging请求。 - 升级后审计: 在双活复制桶上依赖对象标签实施权限隔离(
s3:ExistingObjectTag/*)的集群,建议升级后核查审计日志中是否存在异常的DeleteObjectTagging/PutObjectTagging记录并比对各站点标签一致性。
更多信息请参阅安全公告台账、Server 候选兼容性说明以及关联公告 SN-2026-024(GetObjectAttributes 与 ZIP 归档标签鉴权)。