
Рабочий воспроизводитель на Spring Boot и Quarkus для CVE-2026-94386, демонстрирующий повторное использование OAuth-токена между областями (cross-scope) в статическом кэше токенов Apache Camel camel-oauth.
Запускаемый воспроизводитель proof-of-concept для уязвимости Apache Camel, при которой
OAuthClientCredentialsTokenResolver сохранял полученные токены client-credentials в статическом кэше,
общем для каждого экземпляра resolver и каждого CamelContext в JVM, и использовал в качестве ключа этого кэша
только token endpoint и client ID — хотя запрос токена также содержит client secret и
запрашиваемую область (scope).
// 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) |
Два профиля, которые используют общую конечную точку токена и один и тот же client ID, но запрашивают разные области доступа или аутентифицируются с разными client secret, сопоставляются с одной и той же записью кэша. Профиль, который разрешился первым, заполнил её; другому затем был выдан этот токен без выполнения запроса токена в рамках его собственной конфигурации, на всё время, пока токен оставался действительным.
Кэширование токенов включено по умолчанию, а направление нескольких профилей на один клиент провайдера идентификации — это обычное использование поддержки профилей, а не явное включение небезопасной настройки.
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.
Для воспроизведения проблемы требуется развёртывание, в котором определено более одного профиля, совместно использующих конечную точку токенов и client ID, но различающихся областью действия или клиентским секретом.
Профили настраиваются при развёртывании, которому доверяют, поэтому никакие недоверенные входные данные не попадают в кэш. Выигрывает та служба, которая получает токен, не предназначенный для неё; если она враждебна или скомпрометирована, она может повторно использовать этот токен в пределах своей области действия. Результат — исходящий запрос, несущий токен, выпущенный для области действия другого профиля — с более широкими полномочиями, чем было настроено для вызывающего профиля, либо для службы, для которой токен не выпускался. Если поставщики идентификации выводят аудиторию токена из запрошенной области действия, другая область действия может означать токен для совершенно другой службы.
Ни поставщика идентификации, ни сети. Приложение размещает заглушку конечной точки токенов поставщика, обслуживающую grant типа client credentials, которая выпускает токен, значение которого называет область действия и клиентский секрет, для которых он выпущен, и записывает каждый запрос. Работают настоящий OAuthClientCredentialsTokenResolver и настоящий статический кэш.
Заглушка читает клиентские учётные данные из заголовка HTTP Basic authorization, а не из параметров формы, поскольку именно так их отправляет
camel-oauth.
| Свойство | Значение |
|---|---|
| Компонент | camel-oauth (OAuthClientCredentialsTokenResolver) |
| CWE | CWE-524 (Use of Cache Containing Sensitive Information) |
| Серьёзность | Средняя |
| Вектор атаки | Два или более OAuth-профиля, совместно использующих конечную точку токенов и client ID, но различающихся областью действия или клиентским секретом |
| Воздействие | Исходящий запрос, несущий токен, выпущенный для области действия другого профиля или полученный с секретом другого профиля |
| Затронутые версии | С 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
Ключ кэша теперь содержит все входные данные запроса токена: конечную точку токенов, client 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