
أداة إثبات مفهوم قابلة للتشغيل لإعادة إنتاج CVE-2026-95663، حيث كان Apache Camel camel-keycloak يقدّم نتائج فحص الرموز المخزّنة مؤقتًا بعد انتهاء صلاحية الرمز، مع عروض توضيحية لـ Spring Boot وQuarkus.
أداة إثبات مفهوم قابلة للتشغيل لإعادة إنتاج ثغرة Apache Camel حيث كان KeycloakSecurityPolicy مع
تفعيل استبطان الرمز (useTokenIntrospection=true) يواصل قبول رمز وصول بعد انتهاء صلاحيته،
لأن نتيجة الاستبطان المخزّنة مؤقتًا كانت تعيش أطول من الرمز نفسه.
| بيئة التشغيل | الدليل | الحزمة التقنية |
|---|---|---|
| Camel Spring Boot | camel-spring-boot/ | Spring Boot 3.5.13 + camel-keycloak 4.22.1 |
| Camel Quarkus | camel-quarkus/ | Camel Quarkus 3.39.0 (Quarkus 3.39.0, Camel 4.22.0) |
في مسار الاستبطان، يُبلّغ Keycloak عن الرمز بأنه غير نشط بمجرد تجاوز exp الخاص به — لذا فإن
علامة active هي ما يفرض انتهاء الصلاحية. تُخزَّن نتائج الاستبطان مؤقتًا افتراضيًا
(introspectionCacheEnabled=true، introspectionCacheTtl=60)، وكلا الذاكرتين المؤقتتين المضمّنتين
(ConcurrentMapTokenCache، CaffeineTokenCache) كانتا تُنهيان صلاحية الإدخال بعد مدة ثابتة منذ إدراجه،
دون تقييد ذلك العمر بادّعاء exp الذي كانت الاستجابة المخزّنة تحمله أصلًا.
وبدوره، كان KeycloakSecurityProcessor يتحقق من active فقط ولا يقارن exp بالوقت الحالي أبدًا.
لذلك كانت الاستجابة المخزّنة مؤقتًا قبل انتهاء صلاحية الرمز بفترة وجيزة تُقدَّم للطلبات اللاحقة التي تحمل
الرمز نفسه، فتُقبل بالأدوار والصلاحيات التي كان الرمز المنتهي يحملها — رغم أن RFC 7662
ينص على أن الاستجابة التي تحتوي على exp يجب ألا تُخزَّن مؤقتًا بعد ذلك الوقت.
cd camel-spring-boot # or: cd camel-quarkus
mvn clean package
docker compose up -d --build
until [ "$(curl -s http://localhost:8080/exploit/ready)" = "Started" ]; do sleep 1; done
curl -s http://localhost:8080/exploit/attack # takes ~4s: it waits for the token to expire
docker compose down
على إصدار متأثر (متطابق على كلا زمني التشغيل):
1) Request while the token is still valid
introspection calls so far: 1
outcome: [ADMITTED - the protected route ran]
... the token's exp has now passed. The cache entry is still inside its 60s TTL.
2) The same token again, now expired, against the SAME route
introspection calls so far: 1 (unchanged - the endpoint was never asked again)
outcome: [ADMITTED - the protected route ran]
an expired token was admitted: true
3) CONTROL - the same expired token against a route with introspectionCacheEnabled=false
introspection calls so far: 2 (the endpoint was asked)
outcome: [REJECTED - CamelAuthorizationException: Token is not active (may be revoked or expired)]
rejected once the endpoint was actually consulted: true
>>> PROVEN: ...
عدّاد نداءات الاستبطان هو الدليل: فهو لا يتحرك بين الفحص 1 والفحص 2. لم يُسأل مزوّد الهوية مرة ثانية قط، لذا فإن حكمه — الذي صار بحلول ذلك الحين "غير نشط" — لم يصل إلى السياسة أبدًا. الفحص 3 يثبت أن هذا الحكم موجود: الرمز المنتهي نفسه ضد مسار مع تعطيل التخزين المؤقت يُرفض، وهو أيضًا الحل البديل الذي يقدّمه التنبيه للنشرات التي لا يمكنها الترقية بعد. خفض introspectionCacheTtl يضيّق النافذة فقط؛ ولا يغلقها.
لا خادم Keycloak ولا شبكة. يستضيف التطبيق بديلًا لنقطة نهاية الاستبطان في Keycloak، على المسار الدقيق الذي يبنيه KeycloakTokenIntrospector
({serverUrl}/realms/{realm}/protocol/openid-connect/token/introspect)، والذي يتصرف كما يتصرف Keycloak:
نشط حتى مرور exp، وغير نشط بعده، مع exp وiss وrealm_access.roles في
الاستجابة. تعمل KeycloakSecurityPolicy الحقيقية وKeycloakTokenIntrospector الحقيقي والتخزين المؤقت الحقيقي.
لا يتطلب الاستبطان أن يكون الرمز JWT، لذا فإن الرمز هنا سلسلة مبهمة — الـ exp
المهم يكمن في استجابة الاستبطان، وهو بالضبط ما كان التخزين المؤقت يفشل في احترامه.
لا تمنح المشكلة شيئًا يتجاوز ما كان الرمز يحمله أصلًا: لا تزوير للرموز ولا تصعيد
للصلاحيات. النافذة محدودة بـ introspectionCacheTtl — 60 ثانية افتراضيًا، و300 إلى 600 حيث
اتُّبعت توصية الوثائق للخدمات الداخلية — وهي تُفتح فقط لرمز استبطنه عملية Camel نفسها
وخزّنته مؤقتًا بينما كان لا يزال نشطًا. الاستبطان الرمزي غير مُمكَّن
افتراضيًا. ومن هنا الخطورة المتوسطة.
مسار التحقق المحلي من JWT، المستخدم عند تعطيل الاستبطان، غير متأثر.
| الخاصية | القيمة |
|---|---|
| المكوّن | camel-keycloak (KeycloakSecurityPolicy مع useTokenIntrospection=true) |
| CWE | CWE-613 (انتهاء صلاحية الجلسة غير الكافي) |
| الخطورة | متوسطة |
| ناقل الهجوم | مُستدعٍ يعيد تقديم رمز حامل كان صالحًا قبل لحظات، ضمن مدة صلاحية التخزين المؤقت للاستبطان |
| التأثير | قبول الطلبات بعد انتهاء صلاحية الرمز، بالأدوار والصلاحيات التي كان الرمز يحملها |
| الإصدارات المتأثرة | من 4.16.0 قبل 4.18.5، ومن 4.19.0 قبل 4.22.2 |
| الإصدارات المُصلحة | 4.18.5، 4.22.2، 4.23.0 |
| JIRA | CAMEL-24902 |
| الفضل | Andrea Cosentino من Apache Software Foundation |
التنبيه: https://camel.apache.org/security/CVE-2026-95663.html
تغييران مستقلان:
introspectionCacheTtl
والوقت المتبقي حتى exp الخاص بالرمز، والنتيجة التي انتهت صلاحية رمزها بالفعل لا تُقدَّم
أبدًا من التخزين المؤقت.KeycloakSecurityProcessor الآن رمزًا يقع exp الخاص به في الماضي
على كل مسار يستخدم الاستبطان، لذا يُفرَض انتهاء الصلاحية هناك كما كان مفروضًا بالفعل على مسار
التحقق المحلي من JWT.الاستجابة التي لا تحمل ادعاء exp تحتفظ بمدة الصلاحية المُهيأة.
أُصلح على main في apache/camel#26747 (7bfacb70)، ونُقل
إلى camel-4.22.x في #27182 (66fd7034) وإلى
camel-4.18.x في #27183 (6be02c19).
اضبط camel.version على 4.22.2 وشغّله مرة أخرى. تم التحقق مقابل 4.22.1 → 4.22.2:
2) The same token again, now expired, against the SAME route
introspection calls so far: 2
outcome: [REJECTED - CamelAuthorizationException: Token is not active (may be revoked or expired)]
an expired token was admitted: false
>>> NOT reproduced: the expired token was rejected.
لاحظ أن العداد ينتقل من 1 إلى 2: كان عمر إدخال ذاكرة التخزين المؤقت محدودًا بـ exp الخاص بالرمز المميز، لذا أعاد الطلب الثاني الاستبطان وحصل على الحكم الحقيقي لنقطة النهاية.
يُنشر هذا المستودع لأغراض تعليمية ودفاعية: لمساعدة مستخدمي Apache Camel على فهم الثغرة الأمنية، والتحقق مما إذا كانوا متأثرين بها، وتأكيد أن الترقية تحلّها. لا تستخدم هذه المواد ضد أنظمة لا تملكها أو لا تديرها.