
Reproductor ejecutable de Spring Boot y Quarkus para CVE-2026-94386, que demuestra la reutilización de tokens OAuth entre ámbitos en la caché de tokens estática de camel-oauth de Apache Camel.
Reproductor de prueba de concepto ejecutable para la vulnerabilidad de Apache Camel en la que
OAuthClientCredentialsTokenResolver mantenía los tokens de credenciales de cliente que obtenía en una caché estática
compartida por cada instancia de resolver y cada CamelContext de la JVM, y cuya clave de caché se basaba únicamente en el
endpoint del token y el client ID — aunque la solicitud de token también transporta el client secret y el
ámbito solicitado.
// affected
record TokenCacheKey(String tokenEndpoint, String clientId) { }
| Runtime | Directorio | 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) |
Dos perfiles que comparten un endpoint de token y un client ID pero solicitan scopes diferentes, o se autentican con client secrets diferentes, asignados a la misma entrada de caché. El perfil que resolvió primero la pobló; al otro se le entregó entonces ese token, sin que se realizara ninguna solicitud de token bajo su propia configuración, durante todo el tiempo que el token permaneció válido.
El almacenamiento en caché de tokens está activado por defecto, y apuntar varios perfiles a un mismo cliente de proveedor de identidad es un uso ordinario del soporte de perfiles, no una adhesión a una configuración 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
En una compilación afectada (idéntico en ambos 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: ...
El valor del token nombra el ámbito y el secreto para los que fue emitido, de modo que puedes ver de qué perfil
proviene realmente la configuración de cada token. El registro de solicitudes al final es la evidencia
contundente: con la caché activada, nunca se solicitó al proveedor de identidad el token del perfil admin
— solo aparecen la solicitud del primer perfil y la del control.
La sonda 2 utiliza deliberadamente una segunda instancia del resolver: la caché es static, por lo que una
instancia separada — y un CamelContext separado — la comparten.
El resolver obtiene tokens en nombre de los componentes que autentican sus llamadas salientes con un perfil
OAuth con nombre — camel-openai, camel-langchain4j-agent, camel-huggingface, camel-docling,
camel-a2a, camel-spring-ai-chat y los componentes de IBM watsonx — y de las aplicaciones que llaman
directamente al SPI OAuthClientAuthenticationFactory.
Alcanzar el problema requiere un despliegue que defina más de un perfil que comparta un endpoint de token y un client ID pero que difiera en el ámbito o en el secreto del cliente.
Los perfiles son configurados por el despliegue, que es de confianza, por lo que ninguna entrada no confiable llega a la caché. Quien se beneficia es el servicio que recibe un token que no estaba destinado a recibir, el cual, si es hostil o está comprometido, puede reutilizar ese token dentro de su ámbito. El resultado es una solicitud saliente que porta un token emitido para el ámbito de otro perfil — una autoridad más amplia que aquella para la que el perfil llamante estaba configurado, o un servicio para el que el token no fue emitido. Con proveedores de identidad que derivan la audiencia de un token a partir del ámbito solicitado, un ámbito distinto puede significar un token para un servicio completamente diferente.
Sin proveedor de identidad y sin red. La aplicación aloja un sustituto del endpoint de token del proveedor que
sirve la concesión de credenciales de cliente, el cual emite un token cuyo valor nombra el ámbito y el secreto
del cliente para los que fue emitido, y registra cada solicitud. Se ejecutan el OAuthClientCredentialsTokenResolver
real y la caché estática real.
El sustituto lee las credenciales del cliente del encabezado de autorización HTTP Basic, no de los parámetros del formulario, porque así es como
camel-oauthlas envía.
| Propiedad | Valor |
|---|---|
| Componente | camel-oauth (OAuthClientCredentialsTokenResolver) |
| CWE | CWE-524 (Uso de caché que contiene información sensible) |
| Gravedad | Media |
| Vector de ataque | Dos o más perfiles OAuth que comparten un endpoint de token y un client ID pero difieren en el ámbito o en el secreto del cliente |
| Impacto | Una solicitud saliente que porta un token emitido para el ámbito de otro perfil u obtenido con el secreto de otro perfil |
| Versiones afectadas | Desde 4.19.0 antes de 4.22.2 |
| Versiones corregidas | 4.22.2, 4.23.0 |
| JIRA | CAMEL-24872 |
| Crédito | Andrea Cosentino de Apache Software Foundation |
Aviso: https://camel.apache.org/security/CVE-2026-94386.html
La clave de caché ahora contiene todas las entradas de la solicitud de token: el endpoint de token, el
client ID, una huella SHA-256 del secreto del cliente — de modo que el secreto en bruto no se conserva en
la caché estática — y el ámbito solicitado. Un ámbito ausente, vacío o en blanco cuenta como un único valor,
porque cada uno de ellos deja el parámetro scope fuera de la solicitud; cualquier otro ámbito se compara
exactamente. Los perfiles que coinciden en los cuatro siguen compartiendo un token entre instancias del
resolver, mientras que los perfiles que difieren en credenciales o ámbito obtienen tokens separados.
Corregido en main en apache/camel#26681 (5e896b2d), con
retroporte a camel-4.22.x en #27779 (b2d9957b).
Establece camel.version en 4.22.2 y ejecútalo de nuevo. Verificado con 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