
Lauffähiger Proof-of-Concept-Reproduzierer für CVE-2026-95663, bei dem Apache Camel camel-keycloak zwischengespeicherte Token-Introspektionsergebnisse über den Token-Ablauf hinaus bereitstellte, mit Spring Boot- und Quarkus-Demos.
Lauffähiger Proof-of-Concept-Reproduzierer für die Apache-Camel-Schwachstelle, bei der KeycloakSecurityPolicy mit
aktivierter Token-Introspektion (useTokenIntrospection=true) ein Access-Token auch nach dessen Ablauf weiterhin
akzeptierte, weil das zwischengespeicherte Introspektionsergebnis den Token überlebte.
| Laufzeitumgebung | Verzeichnis | Stack |
|---|---|---|
| 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) |
Auf dem Introspektionspfad meldet Keycloak ein Token als inaktiv, sobald dessen exp überschritten ist — das
active-Flag ist es also, das den Ablauf durchsetzt. Introspektionsergebnisse werden standardmäßig zwischengespeichert
(introspectionCacheEnabled=true, introspectionCacheTtl=60), und beide mitgelieferten Caches
(ConcurrentMapTokenCache, CaffeineTokenCache) ließen einen Eintrag nach einer festen Zeit nach seiner Einfügung
ablaufen, ohne diese Lebensdauer durch den exp-Claim zu begrenzen, den die zwischengespeicherte Antwort bereits trug.
KeycloakSecurityProcessor wiederum prüfte nur active und verglich exp nie mit der aktuellen Zeit.
Eine Antwort, die kurz vor dem Ablauf des Tokens zwischengespeichert wurde, wurde daher an spätere Anfragen mit
demselben Token ausgeliefert, die mit den Rollen und Berechtigungen zugelassen wurden, die das abgelaufene Token
getragen hatte — obwohl RFC 7662 festlegt, dass eine Antwort, die exp enthält, nicht über diesen Zeitpunkt hinaus
zwischengespeichert werden darf.
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
Auf einem betroffenen Build (identisch auf beiden Runtimes):
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: ...
Der Introspektions-Aufrufzähler ist der Beweis: Er bewegt sich nicht zwischen Probe 1 und Probe 2. Der
Identitätsanbieter wurde nie ein zweites Mal gefragt, sodass sein Urteil — das zu diesem Zeitpunkt „inaktiv" lautet — nie
die Policy erreichte. Probe 3 beweist, dass dieses Urteil existiert: Dasselbe abgelaufene Token gegen eine Route mit
deaktiviertem Caching wird abgelehnt, was zugleich der Workaround ist, den der Advisory für Deployments nennt, die noch
nicht aktualisieren können. Das Verringern von introspectionCacheTtl verkleinert nur das Zeitfenster; es schließt es nicht.
Kein Keycloak-Server und kein Netzwerk. Die App hostet einen Stellvertreter für Keycloaks Introspektions-Endpunkt, auf dem
exakten Pfad, den KeycloakTokenIntrospector aufbaut
({serverUrl}/realms/{realm}/protocol/openid-connect/token/introspect), der sich wie Keycloak verhält:
aktiv, bis exp verstrichen ist, danach inaktiv, mit exp, iss und realm_access.roles in der
Antwort. Die echte KeycloakSecurityPolicy, der echte KeycloakTokenIntrospector und der echte Cache laufen.
Die Introspektion erfordert nicht, dass das Token ein JWT ist, daher ist das Token hier eine opake Zeichenkette — das exp,
auf das es ankommt, liegt in der Introspektionsantwort, und genau das hat der Cache nicht berücksichtigt.
Das Problem gewährt nichts über das hinaus, was das Token bereits trug: keine Token-Fälschung und keine
Rechteausweitung. Das Zeitfenster ist durch introspectionCacheTtl begrenzt — standardmäßig 60 Sekunden, 300 bis 600, wenn die
Empfehlung der Dokumentation für interne Dienste befolgt wurde — und es öffnet sich nur für ein Token, das derselbe
Camel-Prozess introspektiert und zwischengespeichert hat, während es noch aktiv war. Die Token-Introspektion ist nicht
standardmäßig aktiviert. Daher die mittlere Schwere.
Der lokale JWT-Verifizierungspfad, der verwendet wird, wenn die Introspektion deaktiviert ist, ist nicht betroffen.
| Eigenschaft | Wert |
|---|---|
| Komponente | camel-keycloak (KeycloakSecurityPolicy mit useTokenIntrospection=true) |
| CWE | CWE-613 (Unzureichender Sitzungsablauf) |
| Schwere | Mittel |
| Angriffsvektor | Ein Aufrufer, der ein Bearer-Token erneut vorlegt, das Momente zuvor gültig war, innerhalb der TTL des Introspektions-Caches |
| Auswirkung | Anfragen werden nach dem Ablauf des Tokens zugelassen, mit den Rollen und Berechtigungen, die dieses Token trug |
| Betroffene Versionen | Ab 4.16.0 vor 4.18.5, ab 4.19.0 vor 4.22.2 |
| Behobene Versionen | 4.18.5, 4.22.2, 4.23.0 |
| JIRA | CAMEL-24902 |
| Danksagung | Andrea Cosentino von der Apache Software Foundation |
Advisory: https://camel.apache.org/security/CVE-2026-95663.html
Zwei unabhängige Änderungen:
introspectionCacheTtl
und der verbleibenden Zeit bis zum exp des Tokens, und ein Ergebnis, dessen Token bereits abgelaufen ist, wird nie
aus dem Cache ausgeliefert.KeycloakSecurityProcessor nun ein Token, dessen exp in der Vergangenheit liegt,
auf jedem Pfad ab, der die Introspektion verwendet, sodass der Ablauf dort erzwungen wird, wie es auf dem lokalen
JWT-Verifizierungspfad bereits der Fall war.Eine Antwort ohne exp-Claim behält die konfigurierte Time-to-Live bei.
Behoben auf main in apache/camel#26747 (7bfacb70), zurückportiert
auf camel-4.22.x in #27182 (66fd7034) und auf
camel-4.18.x in #27183 (6be02c19).
Setzen Sie camel.version auf 4.22.2 und führen Sie es erneut aus. Verifiziert gegen 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.
Beachtest du, dass der Zähler von 1 auf 2 springt: Die Lebensdauer des Cache-Eintrags war durch das exp des Tokens begrenzt, sodass die zweite Anfrage erneut introspektiert wurde und das tatsächliche Urteil des Endpunkts erhielt.