
Proof of concept e verificatore di vulnerabilità per CVE-2026-18963, un account takeover non autenticato nel flusso reset-credentials di Keycloak. Python in singolo file, componente browser Playwright opzionale.
Un proof of concept e verificatore di vulnerabilità per il bypass del flusso reset-credentials di Keycloak, con un componente browser opzionale che visualizza lo stato risultante in una finestra Chrome reale.
Lo strumento è un singolo file, cve_2026_18963_poc.py, scritto utilizzando la libreria standard di Python. Playwright è l'unica dipendenza di terze parti ed è importato in modo lazy, quindi la parte HTTP funziona su un interprete standard senza nulla installato.
cve_2026_18963_poc.py 1009 lines, Python 3.10 or newer
Riconoscimento. CVE-2026-18963 è stato segnalato a Red Hat da James Paremain, che è accreditato nell'avviso del fornitore. Orevim non ha scoperto né segnalato questa vulnerabilità. Questo repository è una proof of concept indipendente scritta a partire dall'avviso pubblicato.
CVE-2026-18963 è un bypass dell'autenticazione nel flusso di recupero password di Keycloak. Sono due difetti che agiscono insieme.
La nota senza scope. Quando un utente apre la schermata di selezione dell'autenticatore,
Keycloak scrive una nota AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED nella
sessione di autenticazione. La nota è un semplice booleano. Registra che una
schermata di selezione è stata visualizzata, non quale esecuzione l'ha visualizzata. La nota
sopravvive all'esecuzione che l'ha impostata.
Il gestore non protetto. ResetCredentialEmail.action() imposta emailVerified
sull'utente risolto e chiama success() senza verificare l'action token
che il link inviato via email dovrebbe trasportare. Il metodo presuppone di poter essere
raggiunto solo seguendo quel link.
Concatenati, un chiamante non autenticato imposta la nota sull'esecuzione
username, fa avanzare il flusso in modo che l'esecuzione email diventi corrente, quindi
rientra nell'URL di reset. La nota obsoleta fa sì che la schermata di selezione venga renderizzata
legata all'esecuzione email. Inviando un corpo vuoto al
loginAction di quell'esecuzione si invoca direttamente il gestore non protetto. Il flusso avanza
all'azione richiesta UPDATE_PASSWORD senza che nessuna email sia mai stata letta.
| Identificatore | CVE-2026-18963, GHSA-4gv3-mc9p-5wqc |
| Classe | CWE-640, meccanismo debole di recupero password per password dimenticata |
| CVSS v3.1 | 9.1 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Affette | >= 26.0.0, < 26.4.15, >= 26.5.0, < 26.6.6, >= 26.7.0, < 26.7.2 |
| Corrette in | 26.4.15, 26.6.6, 26.7.2. La linea 26.8.0 viene distribuita con la correzione |
| Pacchetto | org.keycloak:keycloak-services (Maven) |
| Pubblicato | 18 agosto 2026 |
| Segnalato da | James Paremain, accreditato nell'avviso di Red Hat |
L'unica precondizione è che il realm offra il recupero password agli utenti non autenticati. Non sono richieste credenziali, token o sessioni precedenti.
Otto richieste, in ordine. I tag corrispondono alle etichette dei passaggi stampate dallo strumento e ai
nomi dei file che scrive sotto --out.
| # | Tag | Metodo | Cosa fa |
|---|---|---|---|
| 1 | login | GET | URL di autorizzazione. Stabilisce la sessione di autenticazione e restituisce il link di reset. |
| 2 | reset | GET | Pagina di reset delle credenziali. Contiene il modulo username. |
| 3 | tryanother | POST | tryAnotherWay=on. Scrive la nota senza scope. |
| 4 | username | POST | L'identificatore. Fa avanzare il flusso in modo che l'esecuzione email diventi corrente. Invia l'email di recupero se l'identificatore viene risolto. |
| 5 | reenter | GET | Di nuovo l'URL di reset. La nota obsoleta fa renderizzare il selettore legato all'esecuzione email. |
| 6 | action | POST | Corpo vuoto al loginAction di quell'esecuzione. Invoca il gestore non protetto. |
| 7 | required-action | GET | Segue il Location verso la pagina UPDATE_PASSWORD. |
| 8 | setpassword | POST | La nuova password. Inviata solo con --set-password. |
Il passaggio 6 è l'exploit, e il corpo vuoto è essenziale. Inviare
authenticationExecution=<id> è invece l'interazione ordinaria del selettore: cambia
l'esecuzione e reinvia l'email di recupero, che non raggiunge mai il
difetto. Il POST vuoto non risponde a nessuna sfida, quindi Keycloak chiama il gestore per
qualunque esecuzione la nota obsoleta abbia lasciato corrente.
# Decide whether the instance is affected. Changes nothing, sends no mail.
./cve_2026_18963_poc.py --url https://sso.example.com --realm demo \
--client-id account-console --check
# Additionally report which identifiers resolve to a real account.
# Resolving accounts receive a recovery email. See the note below.
./cve_2026_18963_poc.py --url https://sso.example.com --realm demo \
--client-id account-console --check --users-file names.txt --rate 0.25
# Reach the set-password form for one account and stop there.
./cve_2026_18963_poc.py --url https://sso.example.com --realm demo \
--client-id account-console --username [email protected]
# Complete the takeover. This locks the account holder out.
./cve_2026_18963_poc.py --url https://sso.example.com --realm demo \
--client-id account-console --username [email protected] \
--set-password 'N3w-passw0rd!'
--check esegue una verifica con un identificatore casuale nel formato
<16 hex>@<8 hex>.invalid. Il TLD .invalid è riservato dalla RFC 2606 e
non può essere risolto, quindi la verifica non può entrare in collisione con un account reale e non può
causare l'invio di email. Stabilisce lo stato della build senza toccare
nessuno.
L'aggiunta di
--userscambia questo. Il passo 4 è la normale sottomissione del modulo di recupero password. Su un realm con SMTP configurato, un identificatore che corrisponde a un utente con un indirizzo email riceve una vera email di recupero password, sia su build affette che corrette. La verifica semplice no, perchéResetCredentialEmail.authenticate()non invia nulla quando l'utente è null. Tratta--check --userscome un flusso che notifica persone reali e conferma che sia nell'ambito della tua autorizzazione prima di eseguirlo.
Lo strumento classifica in base alla risposta al passo 6, il POST vuoto.
| Risposta | Esito | Significato |
|---|---|---|
| 400 | account-absent | L'handler è stato eseguito senza action token e non ha risolto alcun utente. La build è affetta. |
302 con Location | form-reached | L'handler è stato eseguito e l'account viene risolto. Affetta, e questo identificatore è reale. |
| Qualsiasi altro | not-vulnerable | Il POST vuoto non è avanzato. |
| Nessun selettore al passo 5 | not-vulnerable | La nota obsoleta non è stata rispettata. |
| 302 dopo il passo 8 | password-set | La password è cambiata e il realm ha emesso una sessione. |
| 429 oltre il budget di retry | rate-limited | Nessun verdetto. L'esecuzione si è fermata. |
| Un negativo raggiunto dopo qualsiasi throttling | inconclusive | Vedi sotto. |
Il 400 è quello importante. Separa la build è affetta da questo
account esiste, il che consente a --check di stabilire l'esposizione contro un'istanza
senza enumerare un singolo utente reale. La risoluzione degli identificatori è poi una
questione separata, opt-in.
Un negativo è valido solo se nulla è stato soggetto a throttling. Keycloak fa scadere la
sessione di autenticazione secondo il proprio orologio. Un 429 a metà catena costa un backoff, e una
catena che riprende dopo una lunga pausa restituisce esattamente la pagina che restituisce una build corretta.
Riportare ciò come non affetta è l'unica modalità di errore che un checker non deve
avere, quindi qualsiasi negativo raggiunto dopo un evento di throttling viene declassato a
inconclusive con il conteggio dei throttling e il consiglio di rieseguire più lentamente.