
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.
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:
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
Die Vending-Kette (loadFileIOForTableLike → StorageAccessConfigProvider.getStorageAccessConfig → *StorageIntegration.getSubscopedCreds) führt selbst keine allowedLocations-Prüfung durch, sodass die einzige Durchsetzung die zur Commit-Zeit ist — und bis dahin hat der privilegierte Lesevorgang bereits stattgefunden und sein Ergebnis steckt in der Antwort.
Andere Codepfade in derselben Datei machen es richtig: Die View-Erstellung und sendNotificationForTableLike validieren beide, bevor sie die FileIO laden. registerTable war der inkonsistente Pfad. Vollständige Analyse in docs/ANALYSIS.md.
Tabellenpfad — Commit 1dd5feeb (2026-06-02, erstmals in 1.6.0 veröffentlicht) fügte eine Zeile an der richtigen Stelle hinzu:
validateLocationForTableLike(identifier, metadataFileLocation, resolvedParent);
FileIO fileIO = loadFileIOForTableLike(identifier, Set.of(locationDir), ...);
Er kam innerhalb eines Feature-PRs („add RegisterTable overwrite support"), nicht als Security-Fix, daher wurde die Korrektur in 1.6.0 ohne ein Advisory ausgeliefert, das sie benennt.
View-Pfad — Commit 7e822f23 („Validate locations when registering tables and views", #5114), 2026-07-20, erstmals in 1.7.0 veröffentlicht, fügte dieselbe Absicherung zu registerView hinzu und konsolidierte die Post-Parse-Prüfungen für beide Pfade.
1.7.0 enthält außerdem 85a0c292 (#4860, „Fix native catalog credential vending skipping allowedLocations re-validation"), der die damit verbundene Defense-in-Depth-Lücke schließt: Der Credential-Pfad selbst validiert jetzt erneut, statt jedem Aufrufer zu vertrauen. Das ist die strukturelle Hälfte des Problems und ein weiterer Grund, warum 1.7.0 und nicht 1.6.0 die Version ist, auf der man sein sollte.
Die Abdeckung wurde anhand der Release-Tags geprüft: 1dd5feeb ist in 1.6.0 und 1.7.0 enthalten; 7e822f23 und 85a0c292 nur in 1.7.0.
Für alle, die an einer älteren Release-Linie festhalten, werden beide Änderungen als Patches bereitgestellt: 0001 für den Tabellenpfad ≤ 1.5.0, 0002 für den View-Pfad von 1.6.0.
Hinweise für Betreiber — Upgrade-Pfad, Gegenmaßnahmen und wie man in vorhandenen Logs nach Ausnutzung sucht — finden sich in docs/REMEDIATION.md.
Erstellt mit ./scripts/version-matrix.sh; jede Zeile ist ein vollständiger Stack-Start und ein Live-Exploit-Versuch. Siehe docs/AFFECTED-VERSIONS.md.
1.4.1 ist wichtig: Es ist das Release, das CVE-2026-42809 behob, denselben Validieren-nach-Vergabe-Fehler im stage-create-Pfad. Dieser Fix war auf den im Report genannten Endpunkt beschränkt, und das Muster wiederholte sich dann zweimal — register behielt das Verhalten bis 1.6.0, und das neue register-view von 1.6.0 wurde damit geboren.
Was dieser Reproduktor nachweist, und nichts weiter:
register auf ≤ 1.5.0 und über register-view auf 1.6.0.Was es nicht nachweist: das massenhafte Abrufen des vollständigen Inhalts eines Objekts außerhalb des Geltungsbereichs über die API. Zwei spätere Prüfungen (der Metadaten-Standort muss unter dem Tabellenstandort liegen, und der geparste Standort muss in allowedLocations sein) verhindern, dass die Registrierung abgeschlossen wird. Der Aufrufer erhält also ein Orakel plus Metadatenfragmente, nicht das gesamte Dokument. Mit einem auf dem Katalog konfigurierten S3-Endpunkt-Override ist dieselbe Primitive eine serverseitige Request Forgery gegen den konfigurierten Host.
Alles läuft lokal: ein Docker-Compose-Projekt auf Loopback-Ports, eine Wegwerf-S3-Instanz (RustFS) und synthetische „Opfer"-Dateien, die dieses Repository selbst anlegt. Es wird kein externer Host kontaktiert, keine Credentials verlassen die Maschine, und docker compose down -v wird beim Beenden ausgeführt, sofern man nicht --keep übergibt. Die Objekte in victim-data/ sind fabriziert; hier gibt es nirgendwo echte Daten.
Nutze es nur auf Systemen, die dir gehören oder zu deren Test du autorisiert bist.
Gefunden bei einer Überprüfung des CVE-2026-42809-Fixes, gemeldet an das Sicherheitsteam der Apache Software Foundation über den Prozess in der SECURITY.md des Projekts und als CVE-2026-64640 erfasst.
Apache License 2.0 — siehe LICENSE. Die Compose-Umgebung ist vom Apache-Polaris-Quickstart abgeleitet; siehe NOTICE.
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: … |
| Release | register (Tabelle) | register-view | Ergebnis |
|---|
| 1.3.0-incubating | verwundbar | Endpunkt fehlt | verwundbar |
| 1.4.0 | verwundbar | Endpunkt fehlt | verwundbar |
| 1.4.1 | verwundbar | Endpunkt fehlt | verwundbar |
| 1.5.0 | verwundbar | Endpunkt fehlt | verwundbar |
| 1.6.0 | vor der Vergabe validiert | verwundbar | verwundbar |
| 1.7.0 | vor der Vergabe validiert | vor der Vergabe validiert | behoben |