
مُعيد إنتاج قابل للتشغيل بـ Spring Boot وQuarkus لـ CVE-2026-94386، يُوضّح إعادة استخدام رمز OAuth عبر النطاقات في ذاكرة التخزين المؤقت الثابتة للرموز في Apache Camel camel-oauth.
مُعيد إنتاج عملي قابل للتشغيل لإثبات المفهوم لثغرة Apache Camel حيث كان
OAuthClientCredentialsTokenResolver يحتفظ برموز بيانات اعتماد العميل التي حصل عليها في ذاكرة تخزين مؤقت ثابتة (static)
مشتركة بين كل نسخة resolver وكل CamelContext في الـ JVM، وكان يفتح تلك الذاكرة المؤقتة على نقطة نهاية الرمز
ومُعرّف العميل فقط — على الرغم من أن طلب الرمز يحمل أيضًا سر العميل و
النطاق المطلوب.
// affected
record TokenCacheKey(String tokenEndpoint, String clientId) { }
| وقت التشغيل | الدليل | المكدس |
|---|---|---|
| Camel Spring Boot | camel-spring-boot/ | Spring Boot 3.5.13 + camel-oauth 4.22.1 |
| Camel Quarkus | camel-quarkus/ | Camel Quarkus 3.39.0 (Quarkus 3.39.0, Camel 4.22.0) |
ملفان شخصيان (profile) يتشاركان نقطة نهاية رمز (token endpoint) ومعرّف عميل (client ID) واحدًا لكنهما يطلبان نطاقات (scopes) مختلفة، أو يصادقان بـ أسرار عميل (client secrets) مختلفة، مرتبطةً بمدخل ذاكرة تخزين مؤقت (cache entry) واحد. الملف الشخصي الذي حلّ أولًا هو من ملأه؛ ثم سُلِّم الرمز الآخر، دون إجراء أي طلب رمز وفقًا لتكوينه الخاص، طالما بقي الرمز صالحًا.
تخزين الرموز المؤقت مفعّل افتراضيًا، وتوجيه عدة ملفات شخصية إلى عميل واحد لدى مزوّد الهوية هو استخدام عادي لدعم الملفات الشخصية وليس اشتراكًا في إعداد غير آمن.
cd camel-spring-boot # or: cd camel-quarkus
mvn clean package
docker compose up -d --build
until curl -sf http://localhost:8080/exploit/ready >/dev/null; do sleep 1; done
curl -s http://localhost:8080/exploit/attack
docker compose down
الإخراج على بناء متأثر (متطابق على كلا زمني التشغيل):
1) Profile 'invoices' resolves first
token handed to it: TOKEN-FOR[scope=invoice.read;secret=...aaaa]
token requests the IdP has received: 1
2) Profile 'admin' resolves next, through a DIFFERENT resolver instance
token handed to it: TOKEN-FOR[scope=invoice.read;secret=...aaaa]
token requests the IdP has received: 1 (unchanged - no request was made under its own config)
it was handed the token issued for the OTHER profile's scope: true
3) Profile 'partner' - same endpoint and clientId, DIFFERENT client secret (...-zzzz)
token handed to it: TOKEN-FOR[scope=invoice.read;secret=...aaaa]
it was served the token obtained with the other secret, without ever
authenticating with its own: true
4) CONTROL - profile 'admin' again with cache-tokens=false
token handed to it: TOKEN-FOR[scope=admin.write;secret=...aaaa]
it got a token for its own scope: true
Everything the IdP was actually asked for:
- client_id=camel-integration-client scope=invoice.read;secret=...aaaa
- client_id=camel-integration-client scope=admin.write;secret=...aaaa
>>> PROVEN: ...
قيمة الرمز تسمّي النطاق والسرّ الذي صدر من أجله، لذا يمكنك رؤية أي ملف تعريف جاء منه كل رمز فعليًا. سجل الطلبات في النهاية هو الدليل القاطع: مع تفعيل التخزين المؤقت، لم يُسأل مزوّد الهوية عن رمز ملف تعريف admin إطلاقًا — يظهر فقط طلب ملف التعريف الأول وطلب الضابط.
يستخدم الفحص 2 نسخة محلّل ثانية عن قصد: فالتخزين المؤقت static، لذا تتشاركه نسخة منفصلة — وCamelContext منفصل.
يحصل المحلّل على الرموز نيابةً عن المكوّنات التي تصادق على استدعاءاتها الصادرة باستخدام ملف تعريف OAuth مسمّى — camel-openai وcamel-langchain4j-agent وcamel-huggingface وcamel-docling وcamel-a2a وcamel-spring-ai-chat ومكوّنات IBM watsonx — وعن التطبيقات التي تستدعي واجهة SPI OAuthClientAuthenticationFactory مباشرةً.
يتطلب الوصول إلى المشكلة عملية نشر تعرّف أكثر من ملف تعريف يتشارك نقطة نهاية رمز ومعرّف عميل مع اختلافهما في النطاق أو سرّ العميل.
تُضبط ملفات التعريف بواسطة عملية النشر، وهي موثوقة، لذا لا يصل أي إدخال غير موثوق إلى التخزين المؤقت. الطرف المستفيد هو الخدمة التي تتلقى رمزًا لم يكن من المفترض أن تتلقاه، والتي يمكنها، إن كانت معادية أو مخترقة، إعادة استخدام ذلك الرمز ضمن نطاقه. النتيجة هي طلب صادر يحمل رمزًا صادرًا لنطاق ملف تعريف آخر — سلطة أوسع مما كان ملف التعريف المستدعي مهيأً له، أو خدمة لم يصدر الرمز من أجلها. ومع مزوّدي الهوية الذين يستنبطون جمهور الرمز من النطاق المطلوب، قد يعني نطاق مختلف رمزًا لخدمة مختلفة تمامًا.
لا مزوّد هوية ولا شبكة. يستضيف التطبيق بديلًا لنقطة نهاية الرمز الخاصة بالمزوّد يخدم منحة بيانات اعتماد العميل، ويصدر رمزًا تسمّي قيمته النطاق وسرّ العميل الذي صدر من أجله، ويسجّل كل طلب. يعمل OAuthClientCredentialsTokenResolver الحقيقي والتخزين المؤقت الثابت الحقيقي.
يقرأ البديل بيانات اعتماد العميل من ترويسة تفويض HTTP Basic، لا من معاملات النموذج، لأن
camel-oauthيرسلها بهذه الطريقة.
| الخاصية | القيمة |
|---|---|
| المكوّن | camel-oauth (OAuthClientCredentialsTokenResolver) |
| CWE | CWE-524 (Use of Cache Containing Sensitive Information) |
| الخطورة | متوسطة |
| ناقل الهجوم | ملفّا تعريف OAuth أو أكثر يتشاركان نقطة نهاية رمز ومعرّف عميل مع اختلافهما في النطاق أو سرّ العميل |
| الأثر | طلب صادر يحمل رمزًا صادرًا لنطاق ملف تعريف آخر أو تم الحصول عليه بسرّ ملف تعريف آخر |
| الإصدارات المتأثرة | من 4.19.0 قبل 4.22.2 |
| الإصدارات المُصلحة | 4.22.2، 4.23.0 |
| JIRA | CAMEL-24872 |
| الفضل | Andrea Cosentino من Apache Software Foundation |
الإشعار الأمني: https://camel.apache.org/security/CVE-2026-94386.html
يحتفظ مفتاح التخزين المؤقت الآن بكل مدخلات طلب الرمز: نقطة نهاية الرمز، ومعرّف العميل، وبصمة SHA-256 لسرّ العميل — حتى لا يُحتفظ بالسرّ الخام في التخزين المؤقت الثابت — والنطاق المطلوب. يُحتسب النطاق الغائب أو الفارغ أو المكوّن من مسافات فقط كقيمة واحدة، لأن كلًّا منها يستبعد معامل scope من الطلب؛ وأي نطاق آخر يُطابَق بدقة. ملفات التعريف المتفقة على الأربعة جميعًا لا تزال تتشارك رمزًا عبر نسخ المحلّل، بينما تحصل ملفات التعريف المختلفة في بيانات الاعتماد أو النطاق على رموز منفصلة.
أُصلح على main في apache/camel#26681 (5e896b2d)، ونُقل إلى camel-4.22.x في #27779 (b2d9957b).
اضبط camel.version على 4.22.2 وشغّله مرة أخرى. تم التحقق مقابل 4.22.1 → 4.22.2:
2) Profile 'admin' resolves next, through a DIFFERENT resolver instance
token handed to it: TOKEN-FOR[scope=admin.write;secret=...aaaa]
token requests the IdP has received: 2
it was handed the token issued for the OTHER profile's scope: false
3) Profile 'partner' ... token handed to it: TOKEN-FOR[scope=invoice.read;secret=...zzzz]
it was served the token obtained with the other secret ...: false
>>> NOT reproduced: each profile got a token for its own scope and secret.
تصل الآن جميع طلبات الرموز الأربعة إلى مزوّد الهوية، كل منها وفق إعداداته الخاصة.
عطّل تخزين الرموز مؤقتًا في كل ملف تعريف OAuth يتشارك نقطة نهاية رمز ومعرّف عميل مع ملف تعريف آخر — camel.oauth.NAME.cache-tokens=false لملف تعريف مسمّى، وcamel.oauth.cache-tokens=false للملف الافتراضي — وذلك على حساب إرسال طلب رمز عند كل عملية حلّ (يُظهر الفحص 4 أن هذا يعمل).
بدلاً من ذلك، سجّل عميل مزوّد هوية منفصلًا لكل ملف تعريف يحتاج إلى نطاق أو سرّ عميل مختلف، بحيث لا يتشارك أي ملفَي تعريف من هذا القبيل معرّف عميل واحدًا.
نُشر هذا المستودع لأغراض تعليمية ودفاعية: لمساعدة مستخدمي Apache Camel على فهم الثغرة، والتحقق مما إذا كانوا متأثرين بها، وتأكيد أن الترقية تعالجها. لا تستخدم هذه المواد ضد أنظمة لا تملكها أو لا تديرها.