
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와 대조하여 검사하기 전에 해당 경로를 서버 측에서 읽습니다.
자신의 카탈로그에서 테이블을 생성하는 권한만 가진 principal은 Polaris가 카탈로그의 스토리지 자격 증명을 사용하여 해당 카탈로그가 절대 접근할 수 없어야 할 객체 — 다른 테넌트의 프리픽스, 다른 버킷, 스토리지 principal이 도달할 수 있는 모든 것 — 를 읽게 만들 수 있습니다.
| 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 권한을 가진 인증된 principal 하나 — 관리자 불필요 |
1.6.0은 수정이 아닙니다.
register를(기능 PR 안에서 부수적으로) 닫았을 뿐이며 동일한 순서 실수를 가진 새로운register-view엔드포인트를 선보였습니다. 재현 도구는 양쪽 모두를 입증합니다. 1.7.0으로 업그레이드하세요.
요구 사항: Compose v2 플러그인이 있는 Docker, 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 역할은 거의 항상 단일 프리픽스보다 넓습니다.
allowedLocations가 벽입니다. 이 버그는 그 벽을 우회합니다.
테스트 0 — 벽은 실재합니다. s3://tenant-b-private에 명시적 위치를 지정한 테이블
생성은 아무것도 읽히기 전에 403 ForbiddenException으로 거부됩니다. Polaris는 해당
위치가 범위를 벗어났다는 것을 정확히 알고 있습니다.
테스트 1 — register는 그래도 읽습니다. 동일한 principal이 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가 막아야 했던 읽기가 수행된 한 단계 뒤에 위반을 감지한 것입니다.
테스트 2 — 돌아오는 것. 피해자 문서에서 파싱된 두 개의 서로 다른 필드가 호출자에게
그대로 반향됩니다: 테이블이 선언한 location과 write.data.path 속성입니다.
테스트 3 — 스토리지 열거 오라클. 동일한 호출은 allowedLocations 밖에 있는 대상의
각 상태에 따라 다르게 응답합니다:
네 가지 구별 가능한 응답은 호출자가 알 권리가 없는 스토리지에 관한 네 가지 사실을 의미합니다. 실제 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에서 처음 릴리스)은 올바른 위치에 한 줄을 추가했습니다:
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으로 전환해야
하는 또 다른 이유입니다.
포함 범위는 릴리스 태그를 기준으로 확인되었습니다: 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이 중요한 이유: stage-create 경로에서 동일한 발급 후 검증(validate-after-vend)
실수를 수정한 릴리스이기 때문입니다. 그 수정은 보고서의 해당 엔드포인트로만 한정되었고,
이후 그 패턴은 두 번 반복되었습니다 — register는 1.6.0까지 그 동작을 유지했으며,
1.6.0의 새로운 register-view는 그 동작을 가진 채 탄생했습니다.
이 재현 도구가 입증하는 것은 다음과 같으며, 그 이상은 없습니다:
register를 통해,
1.6.0에서는 register-view를 통해.입증하지 않는 것: API를 통한 범위 밖 객체 전체 내용의 대량 검색. 이후의 두 검사
(메타데이터 위치가 테이블 위치 아래에 있어야 하고, 파싱된 위치가 allowedLocations에
있어야 함)가 등록 완료를 막으므로, 호출자는 전체 문서가 아닌 오라클과 메타데이터
조각만 얻게 됩니다. 카탈로그에 S3 엔드포인트 오버라이드가 구성된 경우, 동일한
프리미티브는 구성된 호스트에 대한 서버 측 요청 위조가 됩니다.
모든 것이 로컬에서 실행됩니다: 루프백 포트의 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 | 발급 전 검증됨 | 발급 전 검증됨 | 수정됨 |