针对 Apache Camel 漏洞的可运行概念验证复现程序,该漏洞中
OAuthClientCredentialsTokenResolver 将其获取的客户端凭据令牌保存在一个 static 缓存中,
该缓存由 JVM 中的每个解析器实例和每个 CamelContext 共享,并且该缓存仅以令牌
端点与客户端 ID 作为键——尽管令牌请求还携带了客户端密钥和
请求的作用域。
// 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) |
两个 profile 共享同一个令牌端点和同一个客户端 ID,但请求不同的 scope,或使用不同的客户端密钥进行认证,却被映射到同一个缓存条目。先完成解析的 profile 填充了该条目;随后另一个 profile 便拿到了该令牌,在其自身配置下并未发起任何令牌请求,且只要该令牌仍然有效,这一情况就会持续。
令牌缓存默认开启,而将多个 profile 指向同一个身份提供方客户端,属于 profile 支持的常规用法,并非主动选择不安全的设置。
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: ...
令牌值标明了它被签发时所对应的作用域和密钥,因此你可以看出每个令牌实际上来自哪个 profile 的配置。末尾的请求日志是确凿证据:开启缓存后,身份提供方根本没有被请求过 admin profile 的令牌——只有第一个 profile 的请求和对照组的请求出现。
Probe 2 有意使用了第二个 resolver 实例:缓存是 static 的,因此一个单独的实例——以及一个单独的 CamelContext——会共享它。
该 resolver 代表那些使用具名 OAuth profile 对其出站调用进行身份验证的组件来获取令牌——包括 camel-openai、camel-langchain4j-agent、camel-huggingface、camel-docling、camel-a2a、camel-spring-ai-chat 以及 IBM watsonx 组件——以及直接调用 OAuthClientAuthenticationFactory SPI 的应用程序。
触发该问题需要部署中定义了多个共享同一令牌端点和客户端 ID、但在作用域或客户端密钥上有所不同的 profile。
这些 profile 由部署方配置,而部署方是受信任的,因此没有不受信任的输入进入缓存。获益方是那个收到了本不该收到的令牌的服务,如果该服务具有敌意或已被攻陷,它可以在其作用域内重放该令牌。其结果是,一个出站请求携带了为另一个 profile 的作用域签发的令牌——其权限超出了调用 profile 所配置的范围,或者该令牌并非为该服务签发。对于根据所请求的作用域推导令牌受众的身份提供方而言,不同的作用域可能意味着该令牌是为完全不同的服务签发的。
没有身份提供方,也没有网络。该应用托管了一个替代身份提供方令牌端点的服务,用于处理客户端凭据授权,它签发的令牌值标明了该令牌被签发时所对应的作用域和客户端密钥,并记录每一次请求。真实的 OAuthClientCredentialsTokenResolver 和真实的静态缓存都会运行。
该替代服务从 HTTP Basic 授权头读取客户端凭据,而不是从表单参数读取,因为
camel-oauth就是这样发送它们的。
| 属性 | 值 |
|---|---|
| 组件 | camel-oauth(OAuthClientCredentialsTokenResolver) |
| CWE | CWE-524(使用包含敏感信息的缓存) |
| 严重性 | 中 |
| 攻击向量 | 两个或多个 OAuth profile 共享同一令牌端点和客户端 ID,但在作用域或客户端密钥上有所不同 |
| 影响 | 出站请求携带了为另一个 profile 的作用域签发的令牌,或使用另一个 profile 的密钥获取的令牌 |
| 受影响版本 | 从 4.19.0 起,早于 4.22.2 |
| 修复版本 | 4.22.2、4.23.0 |
| JIRA | CAMEL-24872 |
| 致谢 | Apache Software Foundation 的 Andrea Cosentino |
公告:https://camel.apache.org/security/CVE-2026-94386.html
缓存键现在包含令牌请求的每一项输入:令牌端点、客户端 ID、客户端密钥的 SHA-256 指纹——因此原始密钥不会保留在静态缓存中——以及所请求的作用域。缺失、空或空白的作用域被视为同一个值,因为它们中的每一个都会使 scope 参数不出现在请求中;任何其他作用域都精确匹配。在这四项上完全一致的 profile 仍会跨 resolver 实例共享令牌,而在凭据或作用域上有所不同的 profile 则会获取各自独立的令牌。
已在 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.
现在四个令牌请求都会到达身份提供方,每个请求都使用各自的配置。
在与另一个 profile 共享同一令牌端点和客户端 ID 的每个 OAuth profile 上禁用令牌缓存——命名 profile 使用 camel.oauth.NAME.cache-tokens=false,默认 profile 使用 camel.oauth.cache-tokens=false——代价是每次解析时都要发起一次令牌请求(探测 4 表明此方法有效)。或者,为每个需要不同 scope 或客户端密钥的 profile 注册一个独立的身份提供方客户端,使任意两个此类 profile 都不共享客户端 ID。
本仓库出于教育和防御目的发布:帮助 Apache Camel 用户理解该漏洞、验证自己是否受影响,并确认升级可以解决该问题。请勿将本材料用于您不拥有或运营的系统。