
مُعيد إنتاج يستغل تسليم بيانات الاعتماد قبل التحقق من الموقع في Apache Polaris Iceberg REST، مما يُثبت إمكانية القراءات السحابية عبر المستأجرين والاستعلام عن وجود الدلاء.
registerأداة إعادة إنتاج مستقلة بذاتها وتُشغَّل بأمر واحد لـ CVE-2026-64640: نقاط نهاية Iceberg REST الخاصة بـ register في Apache Polaris تُصدر اعتمادات تخزين سحابي لمسار يقدمه المتصل وتقرأ ذلك المسار من جانب الخادم قبل التحقق منه مقابل allowedLocations في الكتالوج.
يمكن لكيان (principal) لا يملك سوى صلاحية إنشاء الجداول في الكتالوج الخاص به أن يجعل Polaris يستخدم اعتمادات تخزين الكتالوج لقراءة كائنات لم يُسمح للكتالوج بالوصول إليها أبدًا — بادئة مستأجر آخر، أو حاوية (bucket) أخرى، أو أي شيء يمكن لصلاحيات التخزين الوصول إليه.
| 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 على أي كتالوج — بدون صلاحيات إدارية |
1.6.0 ليس إصلاحًا. إنه يغلق
register(بشكل عرضي، داخل PR لميزة) ويصدر نقطة نهايةregister-viewالجديدة كليًا بنفس خطأ الترتيب. أداة إعادة الإنتاج تثبت كلا الجزأين. قم بالترقية إلى 1.7.0.
المتطلبات: Docker مع إضافة Compose v2، و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 يقرؤه على أي حال. يوجّه الكيان نفسه 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 الحقيقي، يصل استطلاع وجود bucket إلى نطاق S3 bucket العام باستخدام اعتمادات الكتالوج.
في بناء مُصلح، يكون كل صف في ذلك الجدول هو نفسه 403 يذكر المسار المطلوب فقط، ولا يعود أي شيء من داخل الكائن — يكتشف السكربت هذه البصمة ويبلغ NOT VULNERABLE — pre-validation observed.
الاختبار 4 — الثغرة نفسها على register-view. أضاف Polaris 1.6.0 نقطة النهاية POST .../namespaces/{ns}/register-view، التي تصل إلى registerView، والتي تقوم بتحميل FileIO وتحليل مستند المتصل دون أي تحقق مسبق — تحديدًا ما توقف registerTable عن فعله للتو. على 1.6.0 يعود كناري العرض (view canary) بالشكل التالي:
{"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 (LocalIcebergCatalog اعتبارًا من 1.6.0)، قبل الإصلاح — 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، لذا فإن آلية الإنفاذ الوحيدة هي تلك التي تتم وقت الالتزام (commit) — وبحلول ذلك الوقت تكون القراءة المميزة قد حدثت وتكون نتيجتها في الاستجابة.
المسارات الشقيقة في الملف نفسه ترتب الأمور بشكل صحيح: إنشاء العرض و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.7.0 وليس 1.6.0 هو الإصدار الذي يجب أن تكون عليه.
تم التحقق من الاحتواء مقابل وسوم الإصدارات: 1dd5feeb موجود في 1.6.0 و1.7.0؛ و7e822f23 و85a0c292 موجودان في 1.7.0 فقط.
لأي شخص مثبّت على خط إصدارات أقدم، كلا التغييرين متاحان كرقعة (patch): 0001 لمسار الجدول في الإصدارات ≤ 1.5.0، و0002 لمسار العرض في 1.6.0.
إرشادات المشغّل — مسار الترقية، والتخفيفات، وكيفية البحث عن استغلال في السجلات الحالية — موجودة في docs/REMEDIATION.md.
أُنتجت باستخدام ./scripts/version-matrix.sh؛ كل صف هو بدء كامل لمجموعة الخدمات ومحاولة استغلال حية. انظر docs/AFFECTED-VERSIONS.md.
1.4.1 مهم: إنه الإصدار الذي أصلح CVE-2026-42809، نفس خطأ التحقق بعد إصدار الاعتمادات في مسار stage-create. كان ذلك الإصلاح مقتصرًا على نقطة النهاية الواردة في التقرير، ثم تكرر النمط مرتين — احتفظ register بالسلوك حتى 1.6.0، ووُلد register-view الجديد في 1.6.0 به.
ما تثبته أداة إعادة الإنتاج هذه، ولا شيء أكثر:
register على الإصدارات ≤ 1.5.0 وعبر register-view على 1.6.0.ما لا تثبته: الاسترجاع الجماعي للمحتوى الكامل لكائن خارج النطاق عبر API. يمنع فحصان لاحقان (يجب أن يقع موقع البيانات الوصفية ضمن موقع الجدول، ويجب أن يكون الموقع المُحلَّل ضمن allowedLocations) اكتمال التسجيل، لذلك يحصل المتصل على نبوءة (oracle) إضافة إلى أجزاء من البيانات الوصفية، لا المستند كاملًا. مع ضبط تجاوز لنقطة نهاية S3 على الكتالوج، تُصبح العملية الأساسية نفسها تزوير طلب من جانب الخادم (SSRF) ضد المضيف المُهيأ.
كل شيء يعمل محليًا: مشروع Docker Compose واحد على منافذ loopback، ومثيل S3 مؤقت (RustFS)، وملفات "ضحية" اصطناعية يزرعها هذا المستودع بنفسه. لا يتم الاتصال بأي مضيف خارجي، ولا تغادر أي اعتمادات الجهاز، ويتم تشغيل docker compose down -v عند الخروج إلا إذا مررت --keep. كائنات victim-data/ مصنّعة؛ لا توجد بيانات حقيقية في أي مكان هنا.
استخدمها على أنظمة تملكها أو مخوَّل لك اختبارها.
اكتُشفت أثناء مراجعة إصلاح CVE-2026-42809، وأُبلغ بها فريق أمن مؤسسة Apache Software Foundation عبر العملية الموضحة في SECURITY.md الخاصة بالمشروع، وتُتبَّع باسم CVE-2026-64640.
رخصة Apache 2.0 — انظر LICENSE. بيئة Compose مشتقة من دليل البدء السريع الخاص بـ Apache Polaris؛ انظر NOTICE.
الاستطلاع (جميعها خارج allowedLocations) | استجابة 1.4.1 |
|---|
| بيانات Iceberg metadata صالحة وموجودة | 403 ForbiddenException + المواقع المستخرجة مُعاد إرسالها |
| المفتاح مفقود | 400 NotFoundException — Location does not exist: … |
| bucket غير موجود | 400 NoSuchBucketException — خطأ خام من S3 SDK |
| موجود لكنه ليس بيانات Iceberg metadata | 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 | تحقق قبل إصدار الاعتمادات | تحقق قبل إصدار الاعتمادات | مُصلح |