Apache Camel の脆弱性に対する実行可能な概念実証(PoC)再現環境です。この脆弱性では、トークンイントロスペクションを有効化した(useTokenIntrospection=true)KeycloakSecurityPolicy が、アクセストークンの有効期限が切れた後もそのトークンを受理し続けていました。これは、キャッシュされたイントロスペクション結果がトークンよりも長く存続していたためです。
| ランタイム | ディレクトリ | スタック |
|---|---|---|
| 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) |
イントロスペクション経路では、Keycloak はトークンの exp が過ぎるとそのトークンを inactive として報告します。つまり、有効期限を強制しているのは active フラグです。イントロスペクション結果はデフォルトでキャッシュされ(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サーバーもネットワークも使用しない。アプリはKeycloakのイントロスペクションエンドポイントの代替を、KeycloakTokenIntrospector が構築する正確なパス({serverUrl}/realms/{realm}/protocol/openid-connect/token/introspect)でホストしており、これはKeycloakと同様に動作する。すなわち、exp が過ぎるまではactive、その後はinactiveであり、レスポンスには exp、iss、realm_access.roles が含まれる。実際の KeycloakSecurityPolicy、実際の KeycloakTokenIntrospector、そして実際のキャッシュが動作する。
イントロスペクションはトークンがJWTであることを要求しないため、ここでのトークンは不透明な文字列である。重要な exp はイントロスペクションレスポンス内に存在し、まさにキャッシュが尊重できていなかったものである。
この問題はトークンがすでに持っていた以上のものを付与するものではない。トークンの偽造も権限昇格もない。ウィンドウは introspectionCacheTtl によって制限され(デフォルトでは60秒、内部サービス向けのドキュメントの推奨に従った場合は300〜600秒)、同じCamelプロセスがトークンがまだ有効な間にイントロスペクションしてキャッシュしたトークンに対してのみ開く。トークンイントロスペクションはデフォルトでは有効になっていない。したがって深刻度は中程度である。
イントロスペクションが無効な場合に使用されるローカルJWT検証パスは影響を受けない。
| 項目 | 値 |
|---|---|
| コンポーネント | camel-keycloak(useTokenIntrospection=true の KeycloakSecurityPolicy) |
| CWE | CWE-613(不十分なセッション有効期限) |
| 深刻度 | 中 |
| 攻撃ベクトル | 呼び出し元が、イントロスペクションキャッシュのTTL内に、直前に有効だったベアラートークンを再提示する |
| 影響 | トークンの有効期限後に、そのトークンが持っていたロールと権限でリクエストが許可される |
| 影響を受けるバージョン | 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
2つの独立した変更:
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 によって制限されていたため、2回目のリクエストでは再イントロスペクションが行われ、エンドポイントの実際の判定結果が得られました。
このリポジトリは教育および防御目的で公開されています。Apache Camel ユーザーが脆弱性を理解し、影響を受けているかどうかを確認し、アップグレードによって解決されることを検証できるようにするためです。所有または運用していないシステムに対してこの資料を使用しないでください。