Apache Polaris — Cross-Tenant Credential Vending Before Location Validation in Iceberg REST Register (CVE-2026-64640)
Published: 2026-08-09 • Researcher: Reported to the Apache Security Team via the project SECURITY.md
- Severity
- High
- CVE
- CVE-2026-64640
- Category
- cloud
- Affected product
- Apache Polaris (Apache Iceberg REST catalog), registerTable and registerView endpoints
- Affected versions
- Polaris ≤ 1.6.0 (verified on 1.3.0-incubating, 1.4.0, 1.4.1, 1.5.0, 1.6.0)
- Disclosed
- 2026-08-09
- Patch status
- Patched
Tags
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-64640
- https://polaris.apache.org/security/
- https://github.com/apache/polaris/commit/1dd5feeb298652d87804e800f908018f13134dc5
- https://github.com/apache/polaris/commit/7e822f23ab
- https://github.com/apache/polaris/commit/85a0c29257
- https://nvd.nist.gov/vuln/detail/CVE-2026-42809
- https://github.com/
Archive entry
intelseclab/poc-archiveOn this page
Metadata
| Field | Value |
|---|---|
| Date Added | 2026-08-09 |
| Last Updated | 2026-08-09 |
| Author / Researcher | Reported to the Apache Security Team via the project SECURITY.md |
| CVE / Advisory | CVE-2026-64640 |
| Category | cloud |
| Severity | High |
| CVSS Score | 8.1 (CVSSv3.1: AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N) |
| Status | Patched |
| Tags | apache-polaris, iceberg, apache-iceberg, credential-vending, confused-deputy, authorization-bypass, cross-tenant, s3, storage, allowed-locations, CWE-441, CWE-639, CWE-918, ssrf, server-side-read, information-disclosure, register-table, register-view |
| Related | CVE-2026-42809 (same validate-after-vend pattern in the stage-create path, fixed in Polaris 1.4.1). CVE-2026-64640 is the register-path variant of the same architectural mistake. |
Affected Target
| Field | Value |
|---|---|
| Software / System | Apache Polaris (Apache Iceberg REST catalog), registerTable and registerView endpoints |
| Versions Affected | Polaris ≤ 1.6.0 (verified on 1.3.0-incubating, 1.4.0, 1.4.1, 1.5.0, 1.6.0) |
| Language / Platform | Java; reproducer in Bash + Python 3 + Docker Compose |
| Authentication Required | Yes — requires one authenticated principal with TABLE_CREATE or CATALOG_MANAGE_CONTENT on any catalog. No admin privilege needed. |
| Network Access Required | Remote — the Iceberg REST catalog is exposed over HTTP/HTTPS |
Summary
CVE-2026-64640 is a confused-deputy vulnerability in Apache Polaris: the Iceberg REST register endpoints mint cloud storage credentials for a caller-supplied path and read that path server-side before checking it against the catalog’s allowedLocations. A principal whose only privilege is creating tables in its own catalog can make Polaris use the catalog’s storage credentials to read objects the catalog was never allowed to touch — another tenant’s prefix, another bucket, anything the storage IAM principal can reach.
The ordering is the entire bug. Sibling code paths (view creation, sendNotificationForTableLike) validate first and vend second. registerTable and registerView vend first and validate second — and by the time validation runs, the privileged read has already happened and its result is reflected in the error response returned to the caller.
The vulnerability is a direct descendant of CVE-2026-42809, which was the same mistake in the stage-create path. That fix was scoped to the reported endpoint; the pattern survived in registerTable until 1.6.0, and 1.6.0 shipped a brand-new registerView endpoint born with the identical flaw. Comprehensive fix lands in 1.7.0.
Vulnerability Details
Root Cause
IcebergCatalog.registerTable (and registerView from 1.6.0) performs three operations in the wrong order:
| |
The credential-vending chain (loadFileIOForTableLike → StorageAccessConfigProvider.getStorageAccessConfig → *StorageIntegration.getSubscopedCreds) performs no allowedLocations check of its own. The only enforcement is the commit-time one at step 3 — and by then the privileged read has occurred and parsed fields from the victim object are already in the response.
Attack Vector
- Authenticate as any principal with
CATALOG_MANAGE_CONTENTon a catalog whoseallowedLocationsis, say,[s3://bucket123]. - Call
POST /api/catalog/v1/{prefix}/namespaces/{ns}/registerwith ametadataLocationpointing tos3://tenant-b-private/sales/metadata/00007-....metadata.json— a path in another tenant’s bucket, outside the catalog’sallowedLocations. - Polaris mints credentials scoped to the caller’s path, fetches the object, and parses it as Iceberg table metadata — all before the
allowedLocationscheck. - The response is a 403 (because validation rejects the registration), but the error message quotes
s3://tenant-b-private/warehouse/CANARY-64640-...— a string that exists only inside the body of the victim object. The caller never sent that string; Polaris could only produce it by reading and parsing the object. - The caller learns: object existence, bucket existence, and location-shaped fields inside the object — an information oracle covering everything the storage principal can reach.
On Polaris 1.6.0, the register-view endpoint provides the same primitive through a different code path.
Impact
Cross-tenant information disclosure through the catalog’s own storage credentials. A low-privilege principal can:
- Enumerate storage outside its catalog’s
allowedLocations— determine whether arbitrary objects and buckets exist, using the catalog’s credentials to reach the global S3 bucket namespace. - Extract metadata fragments from Iceberg tables it has no right to read — the
locationandwrite.data.pathproperties are echoed in error responses. - Use the catalog as a confused deputy to perform server-side requests against the storage backend, with the catalog’s full IAM authority.
With an S3 endpoint override configured on the catalog, the same primitive becomes a server-side request forgery (SSRF) against the configured endpoint.
Environment / Lab Setup
The reproducer is fully self-contained: Docker Compose stands up a Polaris instance, a throwaway S3 backend (RustFS), and synthetic victim data. Nothing leaves the machine.
| |
Environment Architecture
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 itSetup Steps
| |
Proof of Concept
See
exploit.sh,docker-compose.yml,docs/,patches/,scripts/,victim-data/, andevidence/in this folder — mirrored byte-for-byte from the upstream repository. The upstream README is preserved asupstream-README.md.
Step-by-Step Reproduction
- Bring up the lab — Docker Compose starts Polaris, RustFS (S3 backend), and bootstraps a catalog with
allowedLocations = [s3://bucket123], a low-privilege user, and synthetic victim objects ins3://tenant-b-private. - The script registers a table with
metadataLocationpointing at the victim object outsideallowedLocations. - The response is 403, but the error body quotes
CANARY-64640-...— a string from inside the victim file — proving the server-side read happened before validation. - Probes for missing objects, missing buckets, and non-Iceberg files return four distinguishable error signatures, confirming a storage enumeration oracle.
Exploit Code
The key observation: the request names only the metadata file path. The response quotes paths from inside the metadata file:
| |
The string CANARY-64640-4f1c9e2a-tenant-b-sales appears nowhere in the request. It exists only inside the victim object at s3://tenant-b-private/sales/metadata/00007-.... Polaris could only echo it by fetching and parsing the object — with credentials the caller should never have been able to wield against that path.
Expected Output
[INFO] === Test 0: the wall is real ===
[PASS] Direct table creation with out-of-scope location was refused (403)
[INFO] === Test 1: register reads it anyway ===
[PASS] Canary 'CANARY-64640-4f1c9e2a-tenant-b-sales' found in error response body
[INFO] === Test 2: what comes back ===
[PASS] Location field from victim document echoed in response
[PASS] write.data.path property from victim document echoed in response
[INFO] === Test 3: storage enumeration oracle ===
[PASS] Missing key: 400 NotFoundException
[PASS] Missing bucket: 400 NoSuchBucketException
[PASS] Non-Iceberg object: 503 RuntimeIOException
[INFO] === Test 4: register-view (1.6.0+) ===
[PASS] View canary 'CANARY-64640-VIEW-8d3b7a15-tenant-b' found on 1.6.0
[RESULT] VULNERABLE — credential vending before location validation reproducedOn a patched build (1.7.0), every probe returns a clean 403 naming only the requested path, and the script reports NOT VULNERABLE — pre-validation observed.
Detection and Indicators of Compromise
Remediation
| Action | Detail |
|---|---|
| Patch | Upgrade to Apache Polaris 1.7.0. 1.6.0 fixes the table path only and reintroduces the flaw on the new registerView endpoint. 1.7.0 carries both the registerView guard (7e822f23) and the structural credential-vending re-validation (85a0c292) that closes the defence-in-depth gap. |
| Workaround | Disable the register and register-view endpoints at a reverse proxy if not needed. Restrict CATALOG_MANAGE_CONTENT to trusted principals. Monitor for cross-prefix metadataLocation values in register requests. |
| Verification | Confirm the Polaris version is 1.7.0 or later. Verify that IcebergCatalog.registerTable and registerView call validateLocationForTableLike() before loadFileIOForTableLike(). |
References
- CVE Record — CVE-2026-64640 (NVD)
- Apache Polaris Security Advisories
- Fix commit — 1dd5feeb (table path, first shipped in 1.6.0)
- Fix commit — 7e822f23 (view path, first shipped in 1.7.0)
- Fix commit — 85a0c292 (credential re-validation, first shipped in 1.7.0)
- Related — CVE-2026-42809 (stage-create path, same validate-after-vend pattern)
- Upstream Reproducer Repository
Notes
Verified this session by reading the full reproducer source (exploit.sh, ~460 lines; scripts/lib.sh; docker-compose.yml; victim-data/). The environment is fully self-contained: Docker Compose stands up Polaris, a throwaway RustFS S3 backend, and synthetic victim objects whose canary strings are known and verifiable. No external network contact, no data leaves the machine, docker compose down -v runs on exit.
Malware screen — clean. No obfuscated payloads, no remote downloaders, no credential exfiltration, no miners, no unexpected binaries, no setup.py/install-time side effects. The exploit script performs only HTTP requests to the local Polaris instance on loopback ports. The synthetic victim objects in victim-data/ are hand-crafted Iceberg metadata JSON files with embedded canary strings — benign by design.
Cross-corroboration: the root cause (credential-vending before allowedLocations validation in registerTable/registerView), the affected version range (≤ 1.6.0), the three fix commits, the four-state storage enumeration oracle, and the relationship to CVE-2026-42809 were all verified against the upstream README and the Apache Polaris repository. The 1.6.0 partial-fix and 1.7.0 comprehensive-fix version story was confirmed by checking the fix commits against the release tags: 1dd5feeb is in 1.6.0 and 1.7.0; 7e822f23 and 85a0c292 are in 1.7.0 only.
The Apache 2.0 license and NOTICE file are preserved from upstream. The repository includes two backport patches (patches/0001-*.patch for ≤ 1.5.0 table path, patches/0002-*.patch for 1.6.0 view path) and a version matrix script (scripts/version-matrix.sh) that validates every release tag side by side.
| |