register 中在位置验证之前发放凭证这是一个自包含、单命令的 CVE-2026-64640 复现工具:Apache Polaris 中的 Iceberg REST register 端点会为调用者提供的路径发放云存储凭证,并在根据目录的 allowedLocations 检查该路径之前,于服务端读取该路径。
一个仅拥有在其自身目录中创建表权限的主体,可以迫使 Polaris 使用该目录的存储凭证,去读取该目录本不允许接触的对象——另一个租户的前缀、另一个存储桶,以及存储主体能够访问到的任何内容。
| CVE | CVE-2026-64640 |
| 组件 | polaris-runtime-service — IcebergCatalog / LocalIcebergCatalog:registerTable、registerView |
| 端点 | POST /api/catalog/v1/{prefix}/namespaces/{namespace}/registerPOST /api/catalog/v1/{prefix}/namespaces/{namespace}/register-view(1.6.0+) |
| CWE | CWE-441(混淆代理)、CWE-639(通过用户控制的键绕过授权)、CWE-918(SSRF) |
| 严重性 | 高——CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N(8.1) |
| 受影响版本 | ≤ 1.6.0(已在 1.3.0-incubating、1.4.0、1.4.1、1.5.0、1.6.0 上验证) |
| 修复版本 | 1.7.0 ——1.6.0 仅修复了表路径,并在新的视图路径上重新引入了该缺陷 |
| 所需权限 | 任意目录上具有 TABLE_CREATE / CATALOG_MANAGE_CONTENT 权限的单个已认证主体——无需管理员 |
1.6.0 并非修复。 它关闭了
register(附带地,在一个功能 PR 内),并发布了带有同样顺序错误的全新register-view端点。该复现工具证明了这两个部分。请升级到 1.7.0。
要求:安装了 Compose v2 插件的 Docker、curl、python3、bash。除此之外不需要其他任何东西——环境基于已发布的镜像构建,并在结束后拆除。
./exploit.sh # default: apache/polaris:1.4.1 -> reproduces via register
./exploit.sh --tag 1.6.0 # partially fixed -> reproduces via register-view
./exploit.sh --tag 1.7.0 # comprehensively fixed -> refuses cleanly on both
./scripts/version-matrix.sh # every release, side by side
退出码 0 表示漏洞成功复现,1 表示未复现,2 表示环境未能启动。所有请求和响应都会写入 evidence/<timestamp>-polaris-<tag>/。
catalog tenant_a_catalog allowedLocations = [ s3://bucket123 ]
actor low_priv_user CATALOG_MANAGE_CONTENT on that catalog, nothing else
target s3://tenant-b-private another tenant's bucket:
no allowedLocations entry, no grant, no relation
to the attacker's catalog — but reachable by the
credentials that back it
最后一行是实际场景中的关键。运维人员使用 allowedLocations 限定目录范围;底层的 IAM 角色几乎总是比一个前缀更宽。allowedLocations 是那道墙。而这个漏洞绕过了它。
测试 0——墙是真实存在的。 在 s3://tenant-b-private 中使用显式位置创建表会在读取任何内容之前被拒绝,并返回 403 ForbiddenException。Polaris 完全清楚该位置超出允许范围。
测试 1——register 仍然读取了它。 同一个主体将 register 指向 s3://tenant-b-private/sales/metadata/00007-tenant-b-sales.metadata.json。响应同样是 403——但其中引用了 s3://tenant-b-private/warehouse/CANARY-64640-4f1c9e2a-tenant-b-sales,这个字符串仅存在于该对象的内容中:
{"error":{"message":"Invalid locations '[s3://tenant-b-private/warehouse/sales/data,
s3://tenant-b-private/warehouse/CANARY-64640-4f1c9e2a-tenant-b-sales]' for identifier
'tenant_a_ns.pwn_canary': s3://tenant-b-private/warehouse/sales/data is not in the list
of allowed locations: [s3://bucket123/tenant_a_ns]","type":"ForbiddenException","code":403}}
请求从未提及 warehouse/。Polaris 只能通过使用目录的凭证获取并解析该对象来生成这些字符串。这个 403 表明 Polaris 在读取发生之后(而该读取本应被阻止)才捕获到违规。
测试 2——返回了什么。 从受害者文档中解析出的两个不同字段会被回显给调用者:表声明的 location 和它的 write.data.path 属性。
测试 3——存储枚举预言机。 对于 allowedLocations 之外目标的各种状态,同一个调用会给出不同的应答:
四个可区分的应答意味着调用者无权了解的关于存储的四类事实。在真实的 AWS 上,存储桶存在性探测会使用目录的凭证触及全局 S3 存储桶命名空间。
在已修复的构建版本上,该表格中的每一行都是相同的 403,且只指名请求的路径,对象内部的内容不会返回——脚本会检测到这一特征,并报告 NOT VULNERABLE — pre-validation observed。
测试 4——register-view 上的相同缺陷。 Polaris 1.6.0 新增了 POST .../namespaces/{ns}/register-view,进入 registerView,后者加载 FileIO 并在没有任何事先验证的情况下解析调用者的文档——这正是 registerTable 刚刚停止做的事情。在 1.6.0 上,视图金丝雀返回:
{"error":{"message":"Invalid locations '[s3://tenant-b-private/warehouse/CANARY-64640-VIEW-8d3b7a15-tenant-b]'
for identifier 'tenant_a_ns.pwn_view': … is not in the list of allowed locations:
[s3://bucket123/tenant_a_ns]","type":"ForbiddenException","code":403}}
因此,1.6.0 部署仍然暴露于相同的原语,只不过是通过不同的端点。在 1.4.1 及更早版本中,该端点不存在,脚本也会如实说明。
IcebergCatalog.registerTable(自 1.6.0 起为 LocalIcebergCatalog),在修复之前——registerView 具有相同的结构:
String locationDir = metadataFileLocation.substring(0, lastSlashIndex); // attacker-controlled
...
FileIO fileIO =
loadFileIOForTableLike(
identifier,
Set.of(locationDir), // credentials minted here
resolvedParent,
new HashMap<>(tableDefaultProperties),
Set.of(PolarisStorageActions.READ, PolarisStorageActions.LIST));
InputFile metadataFile = fileIO.newInputFile(metadataFileLocation); // server-side GET
TableMetadata metadata = TableMetadataParser.read(metadataFile); // server-side parse
ops.commit(null, metadata); // allowedLocations checked HERE
凭证发放链路(loadFileIOForTableLike → StorageAccessConfigProvider.getStorageAccessConfig → *StorageIntegration.getSubscopedCreds)本身不执行任何 allowedLocations 检查,因此唯一的强制性检查发生在提交(commit)时——而到那时,特权读取已经发生,其结果已经包含在响应中。
同一文件中的同类路径则顺序正确:视图创建和 sendNotificationForTableLike 都在加载 FileIO 之前进行验证。registerTable 是那个不一致的路径。完整分析见 docs/ANALYSIS.md。
表路径——提交 1dd5feeb(2026-06-02,首次发布于 1.6.0)在正确的位置添加了一行:
validateLocationForTableLike(identifier, metadataFileLocation, resolvedParent);
FileIO fileIO = loadFileIOForTableLike(identifier, Set.of(locationDir), ...);
该修复包含在一个功能 PR(“add RegisterTable overwrite support”)中,而非安全修复,因此 1.6.0 在发布该修正时没有附带任何提及它的安全公告。
视图路径——提交 7e822f23(“Validate locations when registering tables and views”,#5114),2026-07-20,首次发布于 1.7.0,为 registerView 添加了相同的防护,并整合了两条路径的解析后检查。
1.7.0 还包含了 85a0c292(#4860,“Fix native catalog credential vending skipping allowedLocations re-validation”),它填补了相关的纵深防御缺口:凭证路径本身现在会重新验证,而不是信任每个调用者。这是问题的结构性一半,也是应该使用 1.7.0 而不是 1.6.0 的另一个原因。
已对照发布标签检查了包含情况:1dd5feeb 位于 1.6.0 和 1.7.0 中;7e822f23 和 85a0c292 仅位于 1.7.0 中。
对于仍处于较旧版本线的人,这两个更改都以补丁形式提供:0001 适用于 ≤ 1.5.0 的表路径,0002 适用于 1.6.0 的视图路径。
运维人员指南——升级路径、缓解措施以及如何在现有日志中查找利用迹象——见 docs/REMEDIATION.md。
由 ./scripts/version-matrix.sh 生成;每一行都是一次完整的服务栈启动和一次真实的漏洞利用尝试。参见 docs/AFFECTED-VERSIONS.md。
1.4.1 很关键:它修复了 CVE-2026-42809,即 stage-create 路径中同样的“先发放后验证”错误。该修复仅限于报告中的端点,随后该模式又重复了两次——register 一直保留该行为直到 1.6.0,而 1.6.0 新增的 register-view 从诞生起就带有该问题。
本复现工具所证明的,仅此而已:
register,在 1.6.0 上通过 register-view。它没有证明的是:通过 API 批量获取越界对象的完整内容。后续的两项检查(元数据位置必须位于表位置之下,且解析出的位置必须在 allowedLocations 中)会阻止注册完成,因此调用者得到的是一个预言机加元数据片段,而不是完整文档。如果在目录上配置了 S3 端点覆盖,同样的原语就会变成针对所配置主机的服务端请求伪造(SSRF)。
一切都在本地运行:一个运行在回环端口上的 Docker Compose 项目、一个一次性的 S3(RustFS)实例,以及本仓库自行植入的合成“受害者”文件。不会联系任何外部主机,凭证不会离开机器,并且除非传入 --keep,退出时会运行 docker compose down -v。victim-data/ 中的对象均为伪造;此处没有任何真实数据。
请仅在你拥有或经授权测试的系统上使用。
在审查 CVE-2026-42809 修复期间发现,通过项目 SECURITY.md 中所述的流程报告给 Apache 软件基金会安全团队,并跟踪为 CVE-2026-64640。
Apache License 2.0——参见 LICENSE。Compose 环境源自 Apache Polaris 快速入门;参见 NOTICE。
探测条件(均在 allowedLocations 之外) | 1.4.1 响应 |
|---|
| 有效的 Iceberg 元数据,存在 | 403 ForbiddenException + 回显解析出的位置 |
| 键不存在 | 400 NotFoundException — Location does not exist: … |
| 存储桶不存在 | 400 NoSuchBucketException — 原始 S3 SDK 错误 |
| 存在但不是 Iceberg 元数据 | 503 RuntimeIOException — Failed to read file: … |
| 版本 | register(表) | register-view | 结论 |
|---|
| 1.3.0-incubating | 存在漏洞 | 端点不存在 | 存在漏洞 |
| 1.4.0 | 存在漏洞 | 端点不存在 | 存在漏洞 |
| 1.4.1 | 存在漏洞 | 端点不存在 | 存在漏洞 |
| 1.5.0 | 存在漏洞 | 端点不存在 | 存在漏洞 |
| 1.6.0 | 发放前已验证 | 存在漏洞 | 存在漏洞 |
| 1.7.0 | 发放前已验证 | 发放前已验证 | 已修复 |