针对 Apache Camel 漏洞的可运行概念验证复现程序:当启用令牌 introspection(useTokenIntrospection=true)时,KeycloakSecurityPolicy 在访问令牌过期后仍继续认可该令牌,因为缓存的 introspection 结果比令牌存活得更久。
| 运行时 | 目录 | 技术栈 |
|---|---|---|
| Camel Spring Boot | camel-spring-boot/ | Spring Boot 3.5.13 + camel-keycloak 4.22.1 |
| Camel Quarkus | camel-quarkus/ | Camel Quarkus 3.39.0(Quarkus 3.39.0,Camel 4.22.0) |
在 introspection 路径上,一旦令牌的 exp 已过,Keycloak 就会将其报告为 inactive——因此正是 active 标志负责强制执行过期。Introspection 结果默认会被缓存(introspectionCacheEnabled=true、introspectionCacheTtl=60),而两个随附的缓存实现(ConcurrentMapTokenCache、CaffeineTokenCache)都是在条目插入之后经过固定时间使其过期,并未用缓存响应中已携带的 exp 声明来约束该存活时间。而 KeycloakSecurityProcessor 又只检查 active,从不将 exp 与当前时间进行比较。
因此,在令牌即将过期前不久被缓存的响应,会被后续携带同一令牌的请求所使用,这些请求被授予了该过期令牌曾携带的角色和权限——尽管 RFC 7662 规定,包含 exp 的响应不得在该时间之后继续缓存。
cd camel-spring-boot # or: cd camel-quarkus
mvn clean package
docker compose up -d --build
until [ "$(curl -s http://localhost:8080/exploit/ready)" = "Started" ]; do sleep 1; done
curl -s http://localhost:8080/exploit/attack # takes ~4s: it waits for the token to expire
docker compose down
在受影响的构建上输出(两个运行时上完全相同):
1) Request while the token is still valid
introspection calls so far: 1
outcome: [ADMITTED - the protected route ran]
... the token's exp has now passed. The cache entry is still inside its 60s TTL.
2) The same token again, now expired, against the SAME route
introspection calls so far: 1 (unchanged - the endpoint was never asked again)
outcome: [ADMITTED - the protected route ran]
an expired token was admitted: true
3) CONTROL - the same expired token against a route with introspectionCacheEnabled=false
introspection calls so far: 2 (the endpoint was asked)
outcome: [REJECTED - CamelAuthorizationException: Token is not active (may be revoked or expired)]
rejected once the endpoint was actually consulted: true
>>> PROVEN: ...
内省调用计数器就是证据:它在探测 1 和探测 2 之间没有变化。身份提供方从未被第二次询问,因此它的判定——此时已是“inactive”——从未到达策略。探测 3 证明该判定确实存在:同一个已过期的令牌针对关闭缓存的路径会被拒绝,这也是公告为尚无法升级的部署提供的变通方案。降低 introspectionCacheTtl 只会缩小窗口;它并不能关闭窗口。
没有 Keycloak 服务器,也没有网络。应用在 KeycloakTokenIntrospector 构建的精确路径({serverUrl}/realms/{realm}/protocol/openid-connect/token/introspect)上托管了一个 Keycloak 内省端点的替身,其行为与 Keycloak 一致:在 exp 过去之前为 active,之后为 inactive,响应中包含 exp、iss 和 realm_access.roles。真实的 KeycloakSecurityPolicy、真实的 KeycloakTokenIntrospector 和真实的缓存都在运行。
内省不要求令牌是 JWT,因此这里的令牌是一个不透明字符串——真正重要的 exp 位于内省响应中,而这正是缓存未能遵守的内容。
该问题不会授予超出令牌原本携带内容之外的任何权限:没有令牌伪造,也没有权限提升。窗口受 introspectionCacheTtl 限制——默认 60 秒,若遵循文档对内部服务的建议则为 300 到 600 秒——并且仅对同一 Camel 进程在令牌仍有效时内省并缓存过的令牌开放。令牌内省默认未启用。因此严重性为中等。
本地 JWT 验证路径(在内省禁用时使用)不受影响。
| 属性 | 值 |
|---|---|
| 组件 | camel-keycloak(KeycloakSecurityPolicy,useTokenIntrospection=true) |
| CWE | CWE-613(会话过期不充分) |
| 严重性 | 中等 |
| 攻击向量 | 调用方在内省缓存的 TTL 内重新出示片刻之前仍然有效的 bearer 令牌 |
| 影响 | 在令牌过期后仍被接纳的请求,并携带该令牌原有的角色和权限 |
| 受影响版本 | 从 4.16.0 起至 4.18.5 之前,从 4.19.0 起至 4.22.2 之前 |
| 修复版本 | 4.18.5、4.22.2、4.23.0 |
| JIRA | CAMEL-24902 |
| 致谢 | Apache Software Foundation 的 Andrea Cosentino |
公告:https://camel.apache.org/security/CVE-2026-95663.html
两项独立变更:
introspectionCacheTtl 与令牌 exp 剩余时间中的较短者,并且令牌已过期的结果绝不会从缓存中返回。KeycloakSecurityProcessor 现在会在所有使用内省的路径上拒绝 exp 位于过去的令牌,因此过期在该处也像本地 JWT 验证路径一样被强制执行。不携带 exp 声明的响应保留配置的生存时间。
已在 main 上通过 apache/camel#26747(7bfacb70)修复,并回移植到 camel-4.22.x(#27182,66fd7034)和 camel-4.18.x(#27183,6be02c19)。
将 camel.version 设置为 4.22.2 并再次运行。已针对 4.22.1 → 4.22.2 验证:
2) The same token again, now expired, against the SAME route
introspection calls so far: 2
outcome: [REJECTED - CamelAuthorizationException: Token is not active (may be revoked or expired)]
an expired token was admitted: false
>>> NOT reproduced: the expired token was rejected.
注意计数器从 1 变为 2:缓存条目的生命周期受令牌的 exp 限制,因此第二次请求重新进行了内省,并得到了端点的真实判定结果。
本仓库出于教育和防御目的发布:帮助 Apache Camel 用户了解该漏洞、验证自身是否受影响,并确认升级后问题已解决。请勿将本材料用于您不拥有或运营的系统。