
Reproducer eseguibile in Spring Boot e Quarkus per CVE-2026-94386, che dimostra il riutilizzo di token OAuth tra scope diversi nella cache statica dei token di camel-oauth di Apache Camel.
PoC riproducibile ed eseguibile per la vulnerabilità di Apache Camel in cui
OAuthClientCredentialsTokenResolver manteneva i token client-credentials ottenuti in una cache statica
condivisa da ogni istanza del resolver e da ogni CamelContext nella JVM, e usava come chiave di tale cache
solo l'endpoint del token e il client ID — sebbene la richiesta del token trasporti anche il client secret e
lo scope richiesto.
// affected
record TokenCacheKey(String tokenEndpoint, String clientId) { }
| Runtime | Directory | Stack |
|---|---|---|
| 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) |
Due profili che condividono un token endpoint e un client ID ma richiedono scope diversi, oppure si autenticano con client secret diversi, mappati sulla stessa voce di cache. Il profilo che ha risolto per primo l'ha popolata; all'altro è stato poi consegnato quel token, senza che fosse effettuata alcuna richiesta di token con la propria configurazione, per tutto il tempo in cui il token è rimasto valido.
La cache dei token è attiva per impostazione predefinita, e indirizzare più profili verso un unico client di un identity provider è un uso ordinario del supporto ai profili, non un'adesione esplicita a un'impostazione non sicura.
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
Output su una build interessata (identico su entrambi i runtime):
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: ...
Il valore del token indica lo scope e il secret per cui è stato emesso, così puoi vedere da quale profilo
proviene effettivamente la configurazione di ciascun token. Il log delle richieste alla fine è la prova
concreta: con la cache attiva, all'identity provider non è mai stato chiesto il token del profilo admin —
compaiono solo la richiesta del primo profilo e quella del controllo.
La sonda 2 utilizza deliberatamente una seconda istanza del resolver: la cache è static, quindi
un'istanza separata — e un CamelContext separato — la condivide.
Il resolver ottiene i token per conto dei componenti che autenticano le proprie chiamate in uscita con un
profilo OAuth denominato — camel-openai, camel-langchain4j-agent, camel-huggingface, camel-docling,
camel-a2a, camel-spring-ai-chat e i componenti IBM watsonx — e delle applicazioni che chiamano
direttamente la SPI OAuthClientAuthenticationFactory.
Raggiungere il problema richiede un deployment che definisca più di un profilo che condivide un token endpoint e un client ID ma differisce nello scope o nel client secret.
I profili sono configurati dal deployment, che è considerato attendibile, quindi nessun input non attendibile raggiunge la cache. La parte che ne trae vantaggio è il servizio che riceve un token che non avrebbe dovuto ricevere, il quale, se ostile o compromesso, può riprodurre quel token entro il suo scope. Il risultato è una richiesta in uscita che trasporta un token emesso per lo scope di un altro profilo — un'autorità più ampia di quella per cui il profilo chiamante era configurato, oppure un servizio per cui il token non era stato emesso. Con identity provider che derivano l'audience di un token dallo scope richiesto, uno scope diverso può significare un token per un servizio completamente diverso.
Nessun identity provider e nessuna rete. L'app ospita un sostituto del token endpoint del provider che serve
il grant client-credentials, il quale emette un token il cui valore indica lo scope e il client secret per cui
è stato emesso, e registra ogni richiesta. Vengono eseguiti il vero OAuthClientCredentialsTokenResolver e
la vera cache statica.
Il sostituto legge le credenziali del client dall'header di autorizzazione HTTP Basic, non dai parametri del form, perché è così che
camel-oauthle invia.
| Proprietà | Valore |
|---|---|
| Componente | camel-oauth (OAuthClientCredentialsTokenResolver) |
| CWE | CWE-524 (Use of Cache Containing Sensitive Information) |
| Gravità | Media |
| Vettore di attacco | Due o più profili OAuth che condividono un token endpoint e un client ID ma differiscono nello scope o nel client secret |
| Impatto | Una richiesta in uscita che trasporta un token emesso per lo scope di un altro profilo o ottenuto con il secret di un altro profilo |
| Versioni interessate | Dalla 4.19.0 precedenti alla 4.22.2 |
| Versioni corrette | 4.22.2, 4.23.0 |
| JIRA | CAMEL-24872 |
| Credito | Andrea Cosentino della Apache Software Foundation |
Advisory: https://camel.apache.org/security/CVE-2026-94386.html
La chiave della cache ora contiene ogni input della richiesta di token: il token endpoint, il client ID,
un'impronta SHA-256 del client secret — così il secret in chiaro non viene conservato nella cache statica
— e lo scope richiesto. Uno scope assente, vuoto o composto solo da spazi conta come un unico valore, poiché
ciascuno di essi omette il parametro scope dalla richiesta; qualsiasi altro scope viene confrontato
esattamente. I profili che concordano su tutti e quattro condividono ancora un token tra istanze del resolver,
mentre i profili che differiscono nelle credenziali o nello scope ottengono token separati.
Corretto su main in apache/camel#26681 (5e896b2d), con
backport su camel-4.22.x in #27779 (b2d9957b).
Imposta camel.version su 4.22.2 ed eseguilo di nuovo. Verificato su 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.