
Reproducer für CVE-2026-64640 — Apache Polaris Iceberg REST register/register-view gibt Speicheranmeldeinformationen aus und liest einen vom Angreifer gewählten Metadaten-Speicherort, bevor allowedLocations validiert wird (Confused-Deputy-Cross-Tenant-Read). Betroffen ≤ 1.6.0, behoben in 1.7.0.
registerEin eigenständiger Reproduktor für CVE-2026-64640, der mit einem einzigen Befehl läuft: Die register-Endpunkte der Iceberg-REST-API in Apache Polaris vergeben Cloud-Speicher-Credentials für einen vom Aufrufer gelieferten Pfad und lesen diesen Pfad serverseitig, bevor sie ihn gegen die allowedLocations des Katalogs prüfen.
Ein Principal, dessen einziges Privileg das Erstellen von Tabellen in seinem eigenen Katalog ist, kann Polaris dazu bringen, die Speicher-Credentials des Katalogs zu verwenden, um Objekte zu lesen, die der Katalog nie berühren durfte — das Präfix eines anderen Mandanten, ein anderer Bucket, alles, was der Speicher-Principal erreichen kann.
| CVE | CVE-2026-64640 |
| Komponente | polaris-runtime-service — IcebergCatalog / LocalIcebergCatalog: registerTable, registerView |
| Endpunkte | POST /api/catalog/v1/{prefix}/namespaces/{namespace}/registerPOST /api/catalog/v1/{prefix}/namespaces/{namespace}/register-view (1.6.0+) |
| CWE | CWE-441 (Confused Deputy), CWE-639 (Autorisierungsumgehung über benutzerkontrollierten Schlüssel), CWE-918 (SSRF) |
| Schweregrad | Hoch — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N (8.1) |
| Betroffen | ≤ 1.6.0 (verifiziert auf 1.3.0-incubating, 1.4.0, 1.4.1, 1.5.0, 1.6.0) |
| Behoben in | 1.7.0 — 1.6.0 behebt nur den Tabellenpfad und führt den Fehler im neuen View-Pfad wieder ein |
| Erforderliches Privileg | ein authentifizierter Principal mit TABLE_CREATE / CATALOG_MANAGE_CONTENT auf einem beliebigen Katalog — kein Admin erforderlich |
1.6.0 ist kein Fix. Es schließt
register(beiläufig, innerhalb eines Feature-PRs) und liefert den brandneuenregister-view-Endpunkt mit demselben Reihenfolgefehler aus. Der Reproduktor beweist beide Hälften. Upgrade auf 1.7.0.
Voraussetzungen: Docker mit dem Compose-v2-Plugin, curl, python3, bash. Sonst nichts — die Umgebung wird aus veröffentlichten Images erstellt und danach wieder abgebaut.
./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
Exit-Code 0 bedeutet, dass die Schwachstelle reproduziert wurde, 1, dass sie nicht reproduziert wurde, 2, dass die Umgebung nicht hochkommen konnte. Jede Anfrage und Antwort wird nach evidence/<timestamp>-polaris-<tag>/ geschrieben.
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
Diese letzte Zeile ist der realistische Teil. Betreiber grenzen einen Katalog mit allowedLocations ein; die darunterliegende IAM-Rolle ist fast immer breiter als ein einzelnes Präfix. allowedLocations ist die Mauer. Dieser Bug geht darum herum.
Test 0 — die Mauer ist real. Das Erstellen einer Tabelle mit einem expliziten Standort in s3://tenant-b-private wird mit 403 ForbiddenException abgelehnt, bevor irgendetwas gelesen wird. Polaris weiß genau, dass der Standort außerhalb der Grenzen liegt.
Test 1 — register liest es trotzdem. Derselbe Principal richtet register auf s3://tenant-b-private/sales/metadata/00007-tenant-b-sales.metadata.json. Die Antwort ist ebenfalls ein 403 — aber sie zitiert s3://tenant-b-private/warehouse/CANARY-64640-4f1c9e2a-tenant-b-sales, eine Zeichenkette, die nur im Inhalt dieses Objekts existiert:
{"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}}
Die Anfrage erwähnt warehouse/ nie. Polaris konnte diese Zeichenketten nur erzeugen, indem es das Objekt mit den Credentials des Katalogs abrief und parste. Das 403 ist Polaris, das den Verstoß einen Schritt nach dem Lesevorgang abfängt, den es hätte verhindern sollen.
Test 2 — was zurückkommt. Zwei verschiedene Felder, die aus dem Opferdokument geparst werden, werden an den Aufrufer zurückgespiegelt: die deklarierte location der Tabelle und ihre Eigenschaft write.data.path.
Test 3 — ein Speicher-Enumeration-Orakel. Derselbe Aufruf antwortet für jeden Zustand eines Ziels außerhalb von allowedLocations unterschiedlich:
Sonde (alle außerhalb von allowedLocations) | 1.4.1-Antwort |
|---|---|
| gültige Iceberg-Metadaten, vorhanden | 403 ForbiddenException + geparste Standorte zurückgespiegelt |
| Schlüssel fehlt | 400 NotFoundException — Location does not exist: … |
| Bucket existiert nicht | 400 NoSuchBucketException — roher S3-SDK-Fehler |
| vorhanden, aber keine Iceberg-Metadaten | 503 RuntimeIOException — Failed to read file: … |
Vier unterscheidbare Antworten bedeuten vier Fakten über den Speicher, die der Aufrufer nicht erfahren darf. Auf echtem AWS erreicht die Bucket-Existenz-Sonde mit den Credentials des Katalogs den globalen S3-Bucket-Namespace.
Bei einem behobenen Build ist jede Zeile dieser Tabelle dasselbe 403, das nur den angeforderten Pfad nennt, und nichts aus dem Inneren des Objekts kommt zurück — das Skript erkennt diese Signatur und meldet NOT VULNERABLE — pre-validation observed.
Test 4 — derselbe Fehler bei register-view. Polaris 1.6.0 fügte POST .../namespaces/{ns}/register-view hinzu, das registerView erreicht, welches die FileIO lädt und das Dokument des Aufrufers ohne vorherige Validierung parst — genau das, was registerTable gerade aufgehört hatte zu tun. Auf 1.6.0 kommt der View-Canary zurück:
{"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}}
Eine 1.6.0-Bereitstellung ist also über einen anderen Endpunkt weiterhin derselben Primitive ausgesetzt. Auf 1.4.1 und älter existiert der Endpunkt nicht, und das Skript sagt genau das.
IcebergCatalog.registerTable (LocalIcebergCatalog ab 1.6.0), vor dem Fix — registerView hat dieselbe Form:
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