
CVE-2026-94386 के लिए रन करने योग्य Spring Boot और Quarkus रिप्रोड्यूसर, जो Apache Camel camel-oauth के स्टैटिक टोकन कैश में क्रॉस-स्कोप OAuth टोकन पुनःउपयोग को प्रदर्शित करता है।
Apache Camel भेद्यता के लिए रन करने योग्य प्रूफ-ऑफ-कॉन्सेप्ट पुनरुत्पादक, जहाँ
OAuthClientCredentialsTokenResolver ने अपने द्वारा प्राप्त क्लाइंट-क्रेडेंशियल्स टोकन को एक static कैश में रखा
जो JVM में प्रत्येक रिज़ॉल्वर इंस्टेंस और प्रत्येक CamelContext द्वारा साझा किया जाता था, और उस कैश को केवल टोकन
एंडपॉइंट और क्लाइंट ID पर कीड किया — हालाँकि टोकन अनुरोध क्लाइंट सीक्रेट और
अनुरोधित स्कोप भी ले जाता है।
// 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) |
दो प्रोफ़ाइल जो एक ही टोकन एंडपॉइंट और एक ही क्लाइंट ID साझा करते हैं, लेकिन अलग-अलग स्कोप का अनुरोध करते हैं, या अलग-अलग क्लाइंट सीक्रेट के साथ प्रमाणित होते हैं, जो एक ही कैश प्रविष्टि पर मैप किए गए हैं। जिस प्रोफ़ाइल ने पहले समाधान किया, उसने इसे भर दिया; फिर दूसरे को वही टोकन सौंप दिया गया, बिना उसके अपने कॉन्फ़िगरेशन के अंतर्गत कोई टोकन अनुरोध किए, जब तक टोकन वैध रहा।
टोकन कैशिंग डिफ़ॉल्ट रूप से चालू होती है, और कई प्रोफ़ाइलों को एक ही आइडेंटिटी प्रोवाइडर क्लाइंट पर इंगित करना प्रोफ़ाइल समर्थन का एक सामान्य उपयोग है, न कि किसी असुरक्षित सेटिंग का ऑप्ट-इन।
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 कंपोनेंट्स — और उन एप्लिकेशन्स की ओर से जो सीधे OAuthClientAuthenticationFactory SPI को कॉल करते हैं।
इस समस्या तक पहुँचने के लिए ऐसी डिप्लॉयमेंट आवश्यक है जो एक से अधिक प्रोफ़ाइल परिभाषित करती हो जो एक ही टोकन एंडपॉइंट और क्लाइंट ID साझा करती हों, परंतु स्कोप या क्लाइंट सीक्रेट में भिन्न हों।
प्रोफ़ाइल डिप्लॉयमेंट द्वारा कॉन्फ़िगर की जाती हैं, जो विश्वसनीय है, इसलिए कोई भी अविश्वसनीय इनपुट कैश तक नहीं पहुँचता। लाभ पाने वाला पक्ष वह सेवा है जिसे ऐसा टोकन मिलता है जो उसे मिलना नहीं चाहिए था, जो, यदि शत्रुतापूर्ण या समझौता किया गया हो, तो उस टोकन को उसके स्कोप के भीतर फिर से चला सकती है। परिणाम एक आउटबाउंड अनुरोध है जो किसी अन्य प्रोफ़ाइल के स्कोप के लिए जारी टोकन लेकर जाता है — कॉलिंग प्रोफ़ाइल के लिए कॉन्फ़िगर किए गए अधिकार से अधिक व्यापक अधिकार, या ऐसी सेवा जिसके लिए टोकन जारी नहीं किया गया था। उन आइडेंटिटी प्रोवाइडर्स के साथ जो अनुरोधित स्कोप से टोकन का ऑडियंस निकालते हैं, एक भिन्न स्कोप का अर्थ पूरी तरह से भिन्न सेवा के लिए टोकन हो सकता है।
कोई आइडेंटिटी प्रोवाइडर नहीं और कोई नेटवर्क नहीं। ऐप प्रोवाइडर के टोकन एंडपॉइंट के लिए एक स्टैंड-इन होस्ट करता है जो क्लाइंट-क्रेडेंशियल्स ग्रांट परोसता है, जो एक टोकन जारी करता है जिसका मान उस स्कोप और क्लाइंट सीक्रेट का नाम बताता है जिसके लिए वह जारी किया गया था, और हर अनुरोध को रिकॉर्ड करता है। वास्तविक OAuthClientCredentialsTokenResolver और वास्तविक स्टैटिक कैश चलते हैं।
स्टैंड-इन क्लाइंट क्रेडेंशियल्स को HTTP Basic authorization हेडर से पढ़ता है, फ़ॉर्म पैरामीटर्स से नहीं, क्योंकि
camel-oauthउन्हें इसी तरह भेजता है।
| गुण | मान |
|---|---|
| कंपोनेंट | camel-oauth (OAuthClientCredentialsTokenResolver) |
| CWE | CWE-524 (संवेदनशील जानकारी वाले कैश का उपयोग) |
| गंभीरता | मध्यम |
| हमले का वेक्टर | दो या अधिक OAuth प्रोफ़ाइल जो एक ही टोकन एंडपॉइंट और क्लाइंट ID साझा करती हों, परंतु स्कोप या क्लाइंट सीक्रेट में भिन्न हों |
| प्रभाव | एक आउटबाउंड अनुरोध जो किसी अन्य प्रोफ़ाइल के स्कोप के लिए जारी टोकन लेकर जाता है या किसी अन्य प्रोफ़ाइल के सीक्रेट से प्राप्त किया गया है |
| प्रभावित संस्करण | 4.19.0 से 4.22.2 से पहले तक |
| ठीक किए गए संस्करण | 4.22.2, 4.23.0 |
| JIRA | CAMEL-24872 |
| श्रेय | Andrea Cosentino from Apache Software Foundation |
सलाह: https://camel.apache.org/security/CVE-2026-94386.html
कैश कुंजी अब टोकन अनुरोध के प्रत्येक इनपुट को रखती है: टोकन एंडपॉइंट, क्लाइंट ID, क्लाइंट सीक्रेट का 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.
अब सभी चार टोकन अनुरोध अपने-अपने कॉन्फ़िगरेशन के अंतर्गत identity provider तक पहुँचते हैं।