
Reprodutor executável em Spring Boot e Quarkus para a CVE-2026-94386, demonstrando a reutilização de tokens OAuth entre escopos no cache estático de tokens do camel-oauth do Apache Camel.
Prova de conceito executável que reproduz a vulnerabilidade do Apache Camel em que
OAuthClientCredentialsTokenResolver mantinha os tokens de credenciais de cliente que obtinha em um cache estático
compartilhado por todas as instâncias de resolver e por todos os CamelContext na JVM, e utilizava como chave desse cache apenas o endpoint
do token e o ID do cliente — embora a requisição de token também carregue o segredo do cliente e o
escopo solicitado.
// affected
record TokenCacheKey(String tokenEndpoint, String clientId) { }
| Runtime | Diretório | 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) |
Dois perfis que compartilham um endpoint de token e um client ID, mas solicitam escopos diferentes, ou autenticam com client secrets diferentes, mapeados para a mesma entrada de cache. O perfil que resolveu primeiro a populou; o outro recebeu então aquele token, sem que nenhuma requisição de token fosse feita sob sua própria configuração, pelo tempo em que o token permaneceu válido.
O cache de tokens está ativado por padrão, e apontar vários perfis para um único cliente de provedor de identidade é um uso comum do suporte a perfis, e não uma adesão a uma configuração insegura.
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
Saída em um build afetado (idêntica em ambos os runtimes):
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: ...
O valor do token nomeia o escopo e o segredo para os quais foi emitido, para que você possa ver de qual perfil
a configuração de cada token realmente veio. O log de requisições no final é a evidência concreta: com
o cache ativado, o provedor de identidade nunca foi consultado para o token do perfil admin — apenas a requisição do primeiro
perfil e a do controle aparecem.
A sonda 2 usa uma segunda instância de resolver deliberadamente: o cache é static, então uma instância separada —
e um CamelContext separado — o compartilha.
O resolver obtém tokens em nome dos componentes que autenticam suas chamadas de saída com um perfil
OAuth nomeado — camel-openai, camel-langchain4j-agent, camel-huggingface, camel-docling,
camel-a2a, camel-spring-ai-chat e os componentes IBM watsonx — e de aplicações que chamam a
SPI OAuthClientAuthenticationFactory diretamente.
Alcançar o problema exige uma implantação que defina mais de um perfil compartilhando um endpoint de token e client ID, mas diferindo em escopo ou segredo do cliente.
Os perfis são configurados pela implantação, que é confiável, portanto nenhuma entrada não confiável chega ao cache. A parte que se beneficia é o serviço que recebe um token que não deveria receber, o qual, se hostil ou comprometido, pode reutilizar esse token dentro de seu escopo. O resultado é uma requisição de saída carregando um token emitido para o escopo de outro perfil — autoridade mais ampla do que o perfil chamador foi configurado para ter, ou um serviço para o qual o token não foi emitido. Com provedores de identidade que derivam a audiência de um token a partir do escopo solicitado, um escopo diferente pode significar um token para um serviço completamente diferente.
Nenhum provedor de identidade e nenhuma rede. A aplicação hospeda um substituto para o endpoint de token do provedor que atende a
concessão client-credentials, que emite um token cujo valor nomeia o escopo e o segredo do cliente para os quais foi emitido,
e registra cada requisição. O OAuthClientCredentialsTokenResolver real e o cache estático real são executados.
O substituto lê as credenciais do cliente do cabeçalho de autorização HTTP Basic, não dos parâmetros de formulário, porque é assim que o
camel-oauthas envia.
| Propriedade | Valor |
|---|---|
| Componente | camel-oauth (OAuthClientCredentialsTokenResolver) |
| CWE | CWE-524 (Use of Cache Containing Sensitive Information) |
| Severidade | Média |
| Vetor de ataque | Dois ou mais perfis OAuth compartilhando um endpoint de token e client ID, mas diferindo em escopo ou segredo do cliente |
| Impacto | Uma requisição de saída carregando um token emitido para o escopo de outro perfil ou obtido com o segredo de outro perfil |
| Versões afetadas | Da 4.19.0 antes da 4.22.2 |
| Versões corrigidas | 4.22.2, 4.23.0 |
| JIRA | CAMEL-24872 |
| Crédito | Andrea Cosentino da Apache Software Foundation |
Aviso: https://camel.apache.org/security/CVE-2026-94386.html
A chave do cache agora contém todas as entradas da requisição de token: o endpoint de token, o client ID, uma
impressão digital SHA-256 do segredo do cliente — para que o segredo bruto não seja mantido no cache estático — e o
escopo solicitado. Um escopo ausente, vazio ou em branco conta como um único valor, porque cada um deles deixa o
parâmetro scope fora da requisição; qualquer outro escopo é correspondido exatamente. Perfis que concordam em todos os quatro ainda
compartilham um token entre instâncias de resolver, enquanto perfis que diferem em credenciais ou escopo obtêm tokens
separados.
Corrigido em main em apache/camel#26681 (5e896b2d), retroportado
para camel-4.22.x em #27779 (b2d9957b).
Defina camel.version como 4.22.2 e execute novamente. Verificado contra 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.
Todas as quatro solicitações de token agora chegam ao provedor de identidade, cada uma sob sua própria configuração.