
Riproduttore proof-of-concept eseguibile per CVE-2026-95663, in cui Apache Camel camel-keycloak restituiva risultati di introspezione dei token memorizzati nella cache oltre la scadenza del token, con demo Spring Boot e Quarkus.
Riproduttore proof-of-concept eseguibile per la vulnerabilità di Apache Camel in cui KeycloakSecurityPolicy con
l'introspezione dei token abilitata (useTokenIntrospection=true) continuava ad accettare un access token dopo la sua
scadenza, perché il risultato di introspezione in cache sopravviveva al token stesso.
| Runtime | Directory | 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) |
Sul percorso di introspezione, Keycloak segnala un token come inactive una volta trascorso il suo exp — quindi è
il flag active a far rispettare la scadenza. I risultati di introspezione sono memorizzati in cache per impostazione predefinita
(introspectionCacheEnabled=true, introspectionCacheTtl=60), ed entrambe le cache fornite
(ConcurrentMapTokenCache, CaffeineTokenCache) facevano scadere una voce dopo un tempo fisso dal suo inserimento,
senza limitare tale durata al claim exp che la risposta in cache già conteneva.
KeycloakSecurityProcessor a sua volta verificava solo active e non confrontava mai exp con l'ora corrente.
Una risposta memorizzata in cache poco prima della scadenza del token veniva quindi servita alle richieste successive che portavano lo stesso
token, le quali venivano ammesse con i ruoli e i permessi che il token scaduto aveva portato — sebbene la RFC 7662
stabilisca che una risposta contenente exp non deve essere memorizzata in cache oltre tale momento.
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
Output su una build interessata (identico su entrambi i runtime):
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: ...
Il contatore delle chiamate di introspezione è la prova: non si muove tra la sonda 1 e la sonda 2. L'identity provider non è mai stato interrogato una seconda volta, quindi il suo verdetto — che a quel punto è "inactive" — non ha mai raggiunto la policy. La sonda 3 dimostra che quel verdetto esiste: lo stesso token scaduto contro una route con la cache disattivata viene rifiutato, il che è anche il workaround indicato nell'advisory per i deployment che non possono ancora aggiornare. Abbassare introspectionCacheTtl restringe solo la finestra; non la chiude.
Nessun server Keycloak e nessuna rete. L'app ospita un sostituto dell'endpoint di introspezione di Keycloak, sul percorso esatto che costruisce KeycloakTokenIntrospector
({serverUrl}/realms/{realm}/protocol/openid-connect/token/introspect), che si comporta come fa Keycloak:
attivo finché exp non è passato, inattivo successivamente, con exp, iss e realm_access.roles nella
risposta. Vengono eseguiti il vero KeycloakSecurityPolicy, il vero KeycloakTokenIntrospector e la vera cache.
L'introspezione non richiede che il token sia un JWT, quindi il token qui è una stringa opaca — l'exp
che conta risiede nella risposta di introspezione, che è esattamente ciò che la cache non rispettava.
Il problema non concede nulla oltre a ciò che il token già portava: nessuna falsificazione di token e nessuna
escalation di privilegi. La finestra è limitata da introspectionCacheTtl — 60 secondi per impostazione predefinita, da 300 a 600 dove è stata seguita la
raccomandazione della documentazione per i servizi interni — e si apre solo per un token che lo
stesso processo Camel ha sottoposto a introspezione e memorizzato in cache mentre era ancora attivo. L'introspezione dei token non è abilitata per
impostazione predefinita. Da qui la severità media.
Il percorso di verifica JWT locale, utilizzato quando l'introspezione è disabilitata, non è interessato.
| Proprietà | Valore |
|---|---|
| Componente | camel-keycloak (KeycloakSecurityPolicy con useTokenIntrospection=true) |
| CWE | CWE-613 (Insufficient Session Expiration) |
| Severità | Media |
| Vettore di attacco | Un chiamante che ripresenta un bearer token valido pochi istanti prima, entro il TTL della cache di introspezione |
| Impatto | Richieste ammesse dopo la scadenza del token, con i ruoli e i permessi che quel token portava |
| Versioni interessate | Dalla 4.16.0 precedente alla 4.18.5, dalla 4.19.0 precedente alla 4.22.2 |
| Versioni corrette | 4.18.5, 4.22.2, 4.23.0 |
| JIRA | CAMEL-24902 |
| Credito | Andrea Cosentino from Apache Software Foundation |
Advisory: https://camel.apache.org/security/CVE-2026-95663.html
Due modifiche indipendenti:
introspectionCacheTtl
e il tempo rimanente fino all'exp del token, e un risultato il cui token è già scaduto non viene mai
servito dalla cache.KeycloakSecurityProcessor ora rifiuta un token il cui exp è nel passato
su ogni percorso che utilizza l'introspezione, così la scadenza viene applicata lì come già avveniva sul percorso
di verifica JWT locale.Una risposta che non contiene alcun claim exp mantiene il time-to-live configurato.
Corretto su main in apache/camel#26747 (7bfacb70), backportato
su camel-4.22.x in #27182 (66fd7034) e su
camel-4.18.x in #27183 (6be02c19).
Imposta camel.version su 4.22.2 ed eseguilo di nuovo. Verificato su 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.
Nota che il contatore passa da 1 a 2: la durata della voce in cache era limitata dall'exp del token, quindi la
seconda richiesta ha eseguito nuovamente l'introspezione e ha ottenuto il verdetto reale dell'endpoint.
Questo repository è pubblicato per scopi educativi e difensivi: per aiutare gli utenti di Apache Camel a comprendere la vulnerabilità, verificare se sono interessati e confermare che l'aggiornamento la risolve. Non utilizzare questo materiale contro sistemi di cui non si è proprietari o gestori.