
CVE-2026-94386에 대한 실행 가능한 Spring Boot 및 Quarkus 재현 프로그램으로, Apache Camel camel-oauth의 정적 토큰 캐시에서 발생하는 범위 간 OAuth 토큰 재사용을 시연합니다.
OAuthClientCredentialsTokenResolver가 획득한 클라이언트 자격 증명 토큰을 JVM 내의 모든 리졸버 인스턴스와 모든 CamelContext가 공유하는 static 캐시에 보관하고, 토큰 요청이 클라이언트 시크릿과 요청된 스코프도 함께 전달함에도 불구하고 해당 캐시의 키를 토큰 엔드포인트와 클라이언트 ID만으로 설정했던 Apache Camel 취약점에 대한 실행 가능한 개념 증명(PoC) 재현기입니다.
// 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: ...
토큰 값은 해당 토큰이 발급된 스코프와 시크릿의 이름을 담고 있으므로, 각 토큰이 실제로 어느 프로필의
구성에서 나왔는지 확인할 수 있습니다. 마지막의 요청 로그가 확실한 증거입니다: 캐싱이 켜진 상태에서는
ID 공급자에게 admin 프로필의 토큰을 아예 요청하지 않았습니다 — 첫 번째 프로필의 요청과 대조군의 요청만
나타납니다.
Probe 2는 의도적으로 두 번째 리졸버 인스턴스를 사용합니다: 캐시가 static이므로 별도의 인스턴스 —
그리고 별도의 CamelContext — 가 이를 공유합니다.
리졸버는 명명된 OAuth 프로필로 아웃바운드 호출을 인증하는 컴포넌트 — camel-openai, camel-langchain4j-agent, camel-huggingface, camel-docling,
camel-a2a, camel-spring-ai-chat 및 IBM watsonx 컴포넌트 — 와 OAuthClientAuthenticationFactory SPI를 직접 호출하는
애플리케이션을 대신하여 토큰을 획득합니다.
이 문제에 도달하려면 토큰 엔드포인트와 클라이언트 ID를 공유하면서 스코프나 클라이언트 시크릿이 다른 둘 이상의 프로필을 정의한 배포가 필요합니다.
프로필은 신뢰할 수 있는 배포 환경에서 구성되므로 신뢰할 수 없는 입력은 캐시에 도달하지 않습니다. 이득을 보는 쪽은 자신이 받도록 의도되지 않은 토큰을 받는 서비스이며, 해당 서비스가 적대적이거나 침해된 경우 자신의 스코프 내에서 그 토큰을 재생할 수 있습니다. 그 결과는 다른 프로필의 스코프를 위해 발급된 토큰 — 호출 프로필이 구성된 것보다 넓은 권한, 또는 토큰이 발급되지 않은 서비스 — 을 담은 아웃바운드 요청입니다. 요청된 스코프로부터 토큰의 대상(audience)을 도출하는 ID 공급자의 경우, 스코프가 다르면 전혀 다른 서비스를 위한 토큰이 될 수 있습니다.
ID 공급자도 네트워크도 없습니다. 앱은 클라이언트 자격 증명 그랜트를 제공하는 공급자의 토큰 엔드포인트
대역을 호스팅하며, 이 대역은 발급된 스코프와 클라이언트 시크릿의 이름을 값에 담은 토큰을 발급하고 모든
요청을 기록합니다. 실제 OAuthClientCredentialsTokenResolver와 실제 정적 캐시가 실행됩니다.
대역은 클라이언트 자격 증명을 폼 파라미터가 아닌 HTTP Basic 인증 헤더에서 읽습니다.
camel-oauth가 그렇게 전송하기 때문입니다.
| 속성 | 값 |
|---|---|
| 컴포넌트 | camel-oauth (OAuthClientCredentialsTokenResolver) |
| CWE | CWE-524 (민감한 정보를 포함한 캐시 사용) |
| 심각도 | 중간 |
| 공격 벡터 | 토큰 엔드포인트와 클라이언트 ID를 공유하면서 스코프나 클라이언트 시크릿이 다른 둘 이상의 OAuth 프로필 |
| 영향 | 다른 프로필의 스코프를 위해 발급되었거나 다른 프로필의 시크릿으로 획득한 토큰을 담은 아웃바운드 요청 |
| 영향받는 버전 | 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에 도달합니다.
토큰 엔드포인트와 클라이언트 ID를 다른 프로필과 공유하는 모든 OAuth 프로필에서 토큰 캐싱을 비활성화하십시오 — 명명된 프로필의 경우 camel.oauth.NAME.cache-tokens=false, 기본 프로필의 경우 camel.oauth.cache-tokens=false — 이는 매 해석마다 토큰 요청이 발생하는 대가를 치르게 됩니다(probe 4에서 이것이 작동함을 보여줍니다). 또는 서로 다른 스코프나 클라이언트 시크릿이 필요한 각 프로필에 대해 별도의 identity provider 클라이언트를 등록하여, 그러한 프로필 중 어느 두 개도 클라이언트 ID를 공유하지 않도록 하십시오.
이 저장소는 교육 및 방어 목적으로 게시되었습니다: Apache Camel 사용자가 취약점을 이해하고, 영향을 받는지 확인하며, 업그레이드가 이를 해결하는지 확인할 수 있도록 돕기 위해서입니다. 소유하거나 운영하지 않는 시스템에 이 자료를 사용하지 마십시오.