
PoC — غياب التفويض على مخزن مرساة الثقة GPG على مستوى المنصة في Terrapod (GHSA-6qrc-597p-mrp9، CVE-2026-87006، CVSS 6.5).
حالة CVE: مطلوب، في انتظار التعيين. تم نشر هذا الاكتشاف باسم GHSA-6qrc-597p-mrp9. عند تعيين CVE، يُعاد تسمية هذا المستودع إلى
CVE-YYYY-NNNNN-terrapod-PoCوتُستبدل هذه اللافتة برابط CVE.
| الباحث | Dostxodjayev Abdullox (@squeeze440) |
| التنبيه | GHSA-6qrc-597p-mrp9 |
| CVSS 3.1 | 6.5 (متوسط) |
| الضعف | CWE-862, CWE-284 |
الملخص
يسمح غياب التفويض في واجهة إدارة مفاتيح GPG في Terrapod (main @ b36d953، بعد الإصدار v1.3.1) لأي هوية مُصادَق عليها — بما في ذلك مستخدم يمتلك فقط الدور المدمج everyone بدون أي منح صلاحيات، أو رمز runner محدود النطاق — بإنشاء وحذف مدخلات في مخزن مراسي الثقة GPG على مستوى المنصة، المستخدم للتحقق من التوقيعات على كل مزوّد يُنشر في السجل الخاص، عبر POST /api/terrapod/v1/gpg-keys و DELETE /api/terrapod/v1/gpg-keys/{key_id}.
المنتج
Terrapod (mattrobinsonsre/terrapod) — بديل مستضاف ذاتيًا لـ Terraform Enterprise / HCP Terraform.
الإصدار المُختبَر
الالتزام b36d9535dedc31d85a02093d78f04da748492a2d (main)، متقدم بـ 10 التزامات عن أقرب وسم إصدار v1.3.1. (يوجد v1.3.2 كوسم لكنه يقع على فرع release/v1.3 ولم يُدمج بعد في main؛ الخلل موجود في كليهما.)
تقدير CVSS v3.1
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N — 6.4 (متوسط)
PR:L (وليس N): كل طلب لا يزال يحتاج إلى بيانات اعتماد صالحة — جلسة، أو رمز API، أو حتى رمز runner محدود النطاق runtok: — لكن ليس بيانات اعتماد مميزة. I:H/C:N/A:N: يتيح الخلل للمتصل غير المميز إفساد سلامة مخزن مفاتيح التوقيع المشتركة في السجل (حذف مفاتيح شرعية، وإدراج مفاتيحه الخاصة)، لكنه لا يكشف بحد ذاته عن مواد المفاتيح السرية (لا تُسلسَل المفاتيح الخاصة أبدًا في أي استجابة) ولا يُخرج الخدمة عن العمل.
التفاصيل
يُنفّذ services/terrapod/api/routers/gpg_keys.py عمليات CRUD لصفوف GPGKey — المفاتيح العامة المُدرّعة بصيغة ASCII التي يثق بها Terrapod للتحقق من SHA256SUMS.sig المنفصل على كل إصدار مزوّد يُنشر في السجل الخاص (services/terrapod/services/registry_provider_service.py:191-201، _verify_and_store_shasums_signature). كل مسار في الموجّه محمي فقط بـ Depends(get_current_user) — أي "بعض الهويات المُصادَق عليها" — دون أي فحص للدور أو الصلاحية على الإطلاق:
create_gpg_key_endpoint — services/terrapod/api/routers/gpg_keys.py:104-130list_gpg_keys_endpoint — gpg_keys.py:133-146show_gpg_key_endpoint — gpg_keys.py:149-166revoke_gpg_key_endpoint — gpg_keys.py:168-193delete_gpg_key_endpoint — gpg_keys.py:195-213دوال الخدمة الأساسية (services/terrapod/services/gpg_key_service.py:144 create_gpg_key، :316 delete_gpg_key) لا تأخذ أي وسيط للمتصل/الملكية على الإطلاق — لا يمتلك GPGKey عمود namespace أو owner (_gpg_key_to_jsonapi يُثبّت "namespace": "default"؛ حقل namespace المقبول عند الإنشاء يُحلَّل بواسطة نموذج الطلب لكنه لا يُمرَّر أبدًا). مجموعة المفاتيح هي قائمة عالمية مشتركة واحدة للمنصة بأكملها.
قارن هذا مع كل مورد آخر مملوك للمشرف على مستوى المنصة في نفس قاعدة الكود — tokens.py، roles.py، vcs_connections.py، role_assignments.py — التي تحمي جميعها المسارات المُعدِّلة خلف require_admin أو فحص ملكية صريح bound_to == user.email or is_admin. gpg_keys.py هو الموجّه الوحيد في api/routers/ الذي يدير موردًا حساسًا أمنيًا على مستوى المنصة دون أي من ذلك.
سلسلة التأثير: يُجري get_gpg_key_by_key_id() (registry_provider_service.py:195) بحثًا عالميًا غير محدود النطاق — أي مفتاح سُجّل يومًا ما، من أي شخص، هو مرساة ثقة صالحة لأي نشر مزوّد (وهذا بحكم التصميم لتسجيل مفاتيح الناشر الذاتي — رسالة الخطأ تقول حتى add it via /api/terrapod/v1/gpg-keys first). ولأن التسجيل لا يخضع لفحص مصادقة، والحذف كذلك لا يخضع لفحص مصادقة:
require_non_runner تحديدًا لإبعاده عن "نقاط إنشاء وإدارة الموارد" وفقًا لوصفه الخاص في services/terrapod/api/dependencies.py:387-400، لكن هذا الموجّه لا يستخدمه أبدًا) أن يحذف مفتاح التوقيع المسجَّل لأي مستأجر آخر، مما يكسر التحقق من التوقيع لكل إصدار مزوّد نشره ذلك المستأجر بالفعل — هجوم على السلامة عبر المستأجرين دون أي علاقة مطلوبة بـ namespace الهدف.توثّق مجموعة الاختبارات الخاصة بالمشروع هذه الثغرة دون التساؤل عنها — يبني services/tests/api/test_gpg_keys.py اختبارات المسار السعيد للإنشاء/الحذف/الإلغاء باستخدام AuthenticatedUser(roles=["everyone"], ...) (انظر _user()، السطر 23، المستخدم في جميع أنحاء TestCreate/TestDelete/TestRevoke) ويؤكد 201/204 لذلك المستخدم غير المميز — أي أن الاختبارات تؤكد السلوك القابل للاستغلال كصحيح، لكنها لم تُكتب أبدًا لتسأل "هل ينبغي السماح لـ everyone بفعل هذا؟"
إثبات المفهوم
تم التحقق منه ديناميكيًا مقابل التطبيق الحقيقي (Postgres + Redis حقيقيان عبر أداة اختبار التكامل الخاصة بالمشروع docker-compose.test.yml — لا محاكاة، ولا تعديلات غير ذات صلة على كود التطبيق):
services/tests/integration/test_gpg_key_missing_authz_poc.py:
test_everyone_role_user_can_delete_admins_signing_key — يسجّل admin مفتاحًا عامًا PGP حقيقيًا RSA-2048 عبر POST /api/terrapod/v1/gpg-keys (201، مؤكَّد وجوده في Postgres)، ثم يرسل مستخدم ثانٍ مُصادَق عليه بـ roles=["everyone"] فقط (لا admin، ولا منح صلاحيات) DELETE /api/terrapod/v1/gpg-keys/{key_id} وينجح (204)؛ ويُؤكَّد اختفاء الصف من Postgres بعد ذلك.test_everyone_role_user_can_register_new_trusted_key — يسجّل المستخدم غير المميز نفسه مفتاحًا جديدًا تمامًا عبر POST /api/terrapod/v1/gpg-keys (201).docker build -f docker/Dockerfile.test -t terrapod-test:local .
docker compose -f docker-compose.test.yml run --rm test \
pytest tests/integration/test_gpg_key_missing_authz_poc.py -v -m integration
2 passed — صمد كلا التأكيدين (نجاح الحذف غير المصرّح به، واختفاء الصف فعليًا من قاعدة البيانات الحقيقية). لقطة الشاشة: .التأثير
يمكن لأي مستخدم مُصادَق عليه في نسخة Terrapod — بغض النظر عن الدور، أو الوصول إلى مساحة العمل، أو صلاحيات السجل، وصولًا إلى الدور المدمج غير المميز everyone أو رمز runner محدود النطاق لتشغيل واحد — العبث بمخزن مراسي الثقة GPG المشتركة في المنصة: حذف مفتاح التوقيع المسجَّل لفريق آخر (مما يكسر التحقق من توقيع terraform init لكل إصدار مزوّد نشروه بالفعل، على مستوى المنصة) و/أو إضافة مفاتيح جديدة إلى المجموعة الموثوقة. يقوّض هذا ضمان سلسلة التوريد "سجل وحدات ومزوّدين خاص موقّع بـ GPG" الذي يعلن المشروع عنه كميزة رئيسية، دون الحاجة إلى أي صلاحية على مساحة العمل أو السجل من جانب المهاجم.
نقاط الضعف
المعالجة
أضف بوابة صلاحية/دور إلى المسارات المُعدِّلة في services/terrapod/api/routers/gpg_keys.py، بما يطابق النمط المستخدم بالفعل في tokens.py/roles.py/vcs_connections.py — مثل Depends(require_admin) على create_gpg_key_endpoint و delete_gpg_key_endpoint و revoke_gpg_key_endpoint (الإلغاء محمي بشكل منفصل باشتراط شهادة إلغاء ذاتي صالحة، لكن لا ينبغي أن يظل في متناول متصل عشوائي لمفاتيح مستأجرين آخرين). list/show أقل خطورة (كتل armor هي مفاتيح عامة بحكم التصميم) لكن يُحتمل أن تتطلب أيضًا على الأقل require_non_runner للاتساق. فكّر أيضًا في إدخال عمود namespace/owner على GPGKey إذا كان نموذج مفاتيح الناشر الذاتي لكل namespace هو المقصود، بحيث يمكن لمالك namespace إدارة مفتاحه/مفاتيحه فقط بدلًا من القائمة العالمية الواحدة.
الشكر: Dostxodjayev Abdullox
قناة الإبلاغ: وفقًا لـ SECURITY.md، لا تفتح مشكلة عامة. استخدم الإبلاغ الخاص عن الثغرات في GitHub: انتقل إلى https://github.com/mattrobinsonsre/terrapod/security/advisories/new، وانقر على "Report a vulnerability"، واملأ الوصف وخطوات إعادة الإنتاج والإصدارات المتأثرة. (إذا لم يتوفر PVR، تنص السياسة على مراسلة المشرف مباشرة.)
evidence/gpg_key_authz_poc_run3.png