
Reproducer for CVE-2026-64640 — Apache Polaris Iceberg REST register/register-view vends storage credentials and reads an attacker-chosen metadata location before validating allowedLocations (confused-deputy cross-tenant read). Affected ≤ 1.6.0, fixed in 1.7.0.
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 つ — 管理者は不要 |
1.6.0 は修正ではありません。 これは
registerを(偶然にも機能 PR 内で)閉じただけで、同じ順序ミスを抱えた真新しいregister-viewエンドポイントを出荷しています。この再現スクリプトはその両方を証明します。1.7.0 にアップグレードしてください。
要件: Docker(Compose v2 プラグイン)、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 ロールはほとんどの場合 1 つのプレフィックスより広い範囲を持ちます。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 が防ぐはずだった読み取りの 1 ステップ後に違反を検知しているものです。
テスト 2 — 返ってくるもの。 被害者ドキュメントから解析された 2 つの異なるフィールドが呼び出し元に返されます。テーブルが宣言する location と、その write.data.path プロパティです。
テスト 3 — ストレージ列挙オラクル。 同じ呼び出しは、allowedLocations 外のターゲットの状態ごとに異なる応答を返します。
識別可能な 4 つの応答は、呼び出し元が知る権利のないストレージに関する 4 つの事実を意味します。実際の 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 チェックを行わないため、唯一の強制はコミット時のものだけです。そしてその時点では、特権読み取りはすでに発生し、その結果はレスポンスに含まれています。
同じファイル内の類似パスは順序を正しく守っています。ビューの作成と sendNotificationForTableLike はどちらも FileIO をロードする前に検証します。registerTable だけが一貫していませんでした。詳細なウォークスルーは docs/ANALYSIS.md にあります。
テーブルパス — コミット 1dd5feeb(2026-06-02、1.6.0 で初リリース)は、正しい場所に 1 行追加しました。
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.6.0 ではなく 1.7.0 に移行すべきもう 1 つの理由です。
封じ込めはリリースタグに対して確認済みです。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 パスにおける同じ「発行後検証」の誤り)を修正したリリースだからです。その修正はレポート内のエンドポイントに限定され、その後このパターンは 2 回繰り返されました。register は 1.6.0 までその動作を維持し、1.6.0 の新しい register-view は最初からその動作を持って生まれました。
この再現スクリプトが実証することは、次のとおりです。それ以上ではありません。
register 経由では ≤ 1.5.0、register-view 経由では 1.6.0 で発生します。実証しないこと: API を通じた範囲外オブジェクトの完全な内容の一括取得。後続の 2 つのチェック(メタデータロケーションがテーブルロケーションの配下にあること、解析されたロケーションが allowedLocations 内にあること)によって登録は完了しません。したがって、呼び出し元が得られるのはオラクルとメタデータの断片であり、ドキュメント全体ではありません。カタログに S3 エンドポイント上書きが設定されている場合、同じプリミティブは設定されたホストに対するサーバー側リクエストフォージェリになります。
すべてローカルで実行されます。ループバックポート上の 1 つの Docker Compose プロジェクト、使い捨ての S3(RustFS)インスタンス、そしてこのリポジトリ自体が配置する合成の「被害者」ファイル。外部ホストへの接続はなく、資格情報がマシンの外に出ることもありません。--keep を指定しない限り、終了時に docker compose down -v が実行されます。victim-data/ のオブジェクトは捏造されたものであり、ここに実データは一切ありません。
所有している、またはテストを許可されているシステムでのみ使用してください。
CVE-2026-42809 修正のレビュー中に発見し、プロジェクトの SECURITY.md に記載されたプロセスを通じて Apache Software Foundation のセキュリティチームに報告し、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 | 発行前に検証済み | 発行前に検証済み | 修正済み |