CVE-2026-18963: Verified Reproduction
CVE-2026-18963: Keycloak reset-credentials flow: unauthenticated account takeover CWE-640
CVE-2026-18963 is verified against the affected target. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00337.
What Is CVE-2026-18963?
CVE-2026-18963 is a critical-severity vulnerability. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00337).
CVE-2026-18963 Severity
CVE-2026-18963 is rated critical severity.
Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
How to Reproduce CVE-2026-18963
pruva-verify REPRO-2026-00337 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00337/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-18963
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
unauthenticated HTTP requests: POST tryAnotherWay=on, POST username=<victim>, GET refresh of the reset-credentials execution URL, POST to the leaked reset-credential-email execution URL (no action token), POST attacker-chosen password
- GET/POST /realms/cvetest/login-actions/reset-credentials (Keycloak reset-credentials flow; ResetCredentialEmail execution)
How the agent worked
Root Cause and Exploit Chain for CVE-2026-18963
Keycloak's "reset credentials" (forgot-password) flow can be pivoted by an unauthenticated
attacker onto any victim account. Two defects combine: (1) the authenticator-selection
("try another way") state is stored as a plain boolean auth note
(AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED="true"), so a simple GET refresh of the
reset-credentials execution URL re-renders the selector screen for the current flow
execution; and (2) ResetCredentialEmail.action() blindly calls context.success()
without verifying that the request actually arrived via the emailed action token
(ACTION_TOKEN_USER_ID auth note). An attacker who initiates a password reset for a
victim's username can therefore re-enter the selector screen after the reset email was
sent to the victim, POST to the now-reachable reset-credential-email execution without
any action token, force the flow forward to the UPDATE_PASSWORD required action, and
set an attacker-chosen password on the victim account — a full unauthenticated account
takeover. The attacker never sees the victim's reset email.
- Package/component:
org.keycloak:keycloak-services(servicesmodule:org.keycloak.authentication.DefaultAuthenticationFlow,org.keycloak.authentication.authenticators.resetcred.ResetCredentialEmail) - Affected: Keycloak upstream < 26.4.15, 26.5.x, 26.6.0–26.6.5, 26.7.0–26.7.1 (verified at source level); Red Hat build of Keycloak 26.4/26.6 before container 26.6-12; RH-SSO 7; JBoss EAP Expansion Pack (per advisory RHSA-2026:56519/56523/56524)
- Verified affected:
quay.io/keycloak/keycloak:26.7.1(source tag 26.7.1, commit 73f08b397f193712b26d317210dce99898129709) - Verified fixed:
quay.io/keycloak/keycloak:26.7.2(source tag 26.7.2, contains backport of fix commit cf6e4c8be318f1e38c4001730fe6db6930dad050) - Risk: critical (CVSS 9.1). Any unauthenticated remote attacker who knows a victim's username can take over the account (new password + authenticated session), provided the realm has "forgot password" enabled — a default, common configuration.
Impact Parity
- Disclosed/claimed maximum impact: unauthenticated account takeover (authz_bypass).
- Reproduced impact from this run: unauthenticated account takeover — the victim's password was replaced without possessing the reset email link, an authenticated session (authorization code) as the victim was issued, and the new password was verified against the token endpoint (HTTP 200) while the old password stopped working (HTTP 400).
- Parity:
full. - Not demonstrated: nothing material — the claimed impact was fully reproduced.
Root Cause
In the vulnerable code (services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java,
tag 26.7.1):
- When the user clicks "try another way",
processAction()storesAUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "true"(a boolean), not bound to any execution. processFlow()checks onlyBoolean.parseBoolean(note)plus a non-nullCURRENT_AUTHENTICATION_EXECUTIONnote. After the attacker posts the victim's username,ResetCredentialEmail.authenticate()sends the reset email and callscontext.forkWithSuccessMessage(EMAIL_SENT); the FORK handling setsCURRENT_AUTHENTICATION_EXECUTIONto the email execution id. A subsequent GET refresh of the original reset-credentials URL therefore re-renderscreateSelectAuthenticatorsScreen(emailExecution), whose HTML form action leaks the email execution UUID (execution=<uuid>).ResetCredentialEmail.action()(vulnerable) is:
Posting to the leaked email-execution URL (no action token required) invokes this blind success, the required-elements chain continues topublic void action(AuthenticationFlowContext context) { context.getUser().setEmailVerified(true); context.success(); }reset-password/UPDATE_PASSWORD, and the attacker sets a new password on the victim identity attached to the authentication session byResetCredentialChooseUser.
Fix (upstream commit cf6e4c8be318f1e38c4001730fe6db6930dad050, PR #51844, backported
to 26.4.15 / 26.6.6 / 26.7.2):
- The selector note now stores the execution model id, and
processFlow()only re-renders the selector when the note matchesCURRENT_AUTHENTICATION_EXECUTION(otherwise the note is removed) — the GET refresh after the email fork no longer re-renders the selector screen. ResetCredentialEmail.action()now requiresuser.getId().equals(authSession.getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID)), i.e. success is only possible through the emailed action token; otherwise the flow fails withINVALID_USER.
The upstream regression test ResetPasswordTest.resetPasswordTryAnotherWay() served as
the PoC blueprint for the flow-selector re-entry.
Reproduction Steps
bundle/repro/reproduction_steps.sh(self-contained; helperbundle/repro/smtp_sink.pyis a promoted required artifact).- The script:
- Verifies at source level that tag 26.7.1 lacks and tag 26.7.2 contains the fix hunk.
- Starts a dependency-free Python SMTP sink container (captures the victim's reset email, proving the action-token link is generated and sent only to the victim).
- Starts Keycloak 26.7.1 (vulnerable), provisions realm
cvetest(resetPasswordAllowed=true, SMTP → sink), uservictim(emailvictim@cvetest.local, passwordOldPass123!), and a public direct-grant clientatk-cliused only for verification. - Runs the full browser-equivalent attack with curl (cookie jar, per-page rotating
session_codehonored): login page → forgot-password → POSTtryAnotherWay=on→ POSTusername=victim→ GET-refresh the original reset URL → POST the leaked email execution URL → POST the new password. - Asserts the pivot: step-5 refresh re-renders
kc-select-credential-form, step-6 returns302 → login-actions/required-action?execution=UPDATE_PASSWORD, step-7 returns302 → /account/?...&code=...(attacker session as victim), and the token endpoint returns 200 for the new password / 400 for the old one. - Repeats the attack twice (password restored between attempts via admin API).
- Repeats the same procedure twice against Keycloak 26.7.2 (fixed): the refresh must re-render the "You should receive an email shortly" page (no selector) and the victim password must remain unchanged.
- Writes
bundle/repro/runtime_manifest.jsonon every exit (trap) and streams all output tobundle/logs/reproduction_steps.log.
- Expected evidence:
2/2vulnerable attempts EXPLOITED,2/2fixed attempts BLOCKED, exit code 0.
Evidence
bundle/logs/reproduction_steps.log— full transcript of both phases.bundle/logs/vuln-attempt-{1,2}.log— per-attempt exploit transcripts. Key excerpts (attempt 1):step5 GET refresh -> selector markers=1 email-sent markers=0and the re-rendered selector form action now containsexecution=0089b76d-...(the email execution, different from the choose-user execution141b34d5-...used in steps 2–4).step6 POST email execution (no action token) -> HTTP/1.1 302 Found Location: .../login-actions/required-action?execution=UPDATE_PASSWORD...— the blindcontext.success()fired without any action token.step7 new password -> HTTP/1.1 302 Found Location: http://localhost:8080/realms/cvetest/account/?session_state=...&code=...— the attacker is issued an authorization code asvictim.step8 takeover verify: NEW password token HTTP 200, OLD password token HTTP 400.
bundle/logs/fixed-attempt-{1,2}.log—step5 GET refresh -> selector markers=0 email-sent markers=1; old password still 200, new password 400 → BLOCKED.bundle/logs/smtp-capture-final.log— the captured "Reset password" email addressedTo: victim@cvetest.localcontaining the real/realms/cvetest/login-actions/action-token?key=eyJ...link (which the attacker never receives).bundle/logs/http/<role>-<n>/step*.html|*.hdr— raw HTTP responses/headers for every step of every attempt.bundle/logs/keycloak-vuln.log,bundle/logs/keycloak-fixed.log— server logs;server-versionreported by/admin/serverinfowas 26.7.1 / 26.7.2 respectively.bundle/repro/runtime_manifest.json— runtime identity (image digestsquay.io/keycloak/keycloak@sha256:f1f1f01e…vulnerable,…@sha256:83133051…fixed), attempt results, proof artifact list.- Environment: rootless Docker 27.5.1 on Linux x86_64; Keycloak
start-dev(H2), hostnamehttp://localhost:8080; no sanitizers involved (product runtime proof).
Recommendations / Next Steps
- Upgrade to a fixed build: upstream Keycloak ≥ 26.4.15 / ≥ 26.6.6 / ≥ 26.7.2; Red Hat build of Keycloak 26.6-12 container or later (RHSA-2026:56519, 56523, 56524).
- The upstream fix is the correct approach: bind the selector-screen auth note to the
execution model id and require the
ACTION_TOKEN_USER_IDnote inResetCredentialEmail.action(). - Regression testing: keep
ResetPasswordTest.resetPasswordTryAnotherWay()and add an end-to-end test asserting that a GET refresh after the email fork cannot surface the email execution's action URL. - Defense-in-depth: consider invalidating or single-using the email execution state once the action token is issued, and rate-limiting reset-credential initiations per username/IP.
Additional Notes
- Idempotency: the script is fully idempotent — it re-creates the docker network and containers, truncates the SMTP capture, and restores the victim's password between attempts. Verified by two consecutive successful runs (exit 0 both times).
- The exploit uses only unauthenticated endpoints; no SMTP access, no victim interaction, and no knowledge beyond the victim's username are required. The victim's email delivery is not even required to succeed for the pivot (Keycloak forks the flow with a success message even when sending fails), but this reproduction proves real email generation via the SMTP capture.
- The fork behavior (
forkWithSuccessMessage→ cloned tab on the browser flow) is why the legitimate "email sent" page is displayed on a cloned authentication-session tab while the original tab retains the selector note and the email execution as current — the exact state the GET refresh abuses. - The verification client
atk-cli(public, direct grants) exists only to prove the password change; it plays no role in the exploit path itself.
CVE-2026-18963 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
docker --version 2>&1; echo "---"; docker info 2>&1 | head -20; echo "---"; java -version 2>&1; echo "---"; docker images 2>&1 | head -20Docker version 29.1.3, build 29.1.3-0ubuntu4.1 --- Client: Version: 29.1.3 Context: default Debug Mode: false Plugins: trust: Manage trust on Docker images (Docker Inc.)
git ls-remote https://github.com/keycloak/keycloak.git 2>&1 | grep -E "cf6e4c8|refs/tags/26\.[0-9]" | head -30; echo "EXIT: $?"{"cwd":"/workspace","duration_ms":1645,"exit_code":0,"stderr":"","stdout":"cf6e4c8be318f1e38c4001730fe6db6930dad050\trefs/pull/51844/head\n632f214aa2d668e8b59920c3dfcc449da68254a4\trefs/tags/26.0.0\n715b95b4e6fd333658f5e0649caa8f3d1761e3ff\trefs/tags/26.0.1\nb8563422152a91683d46ece89f257b343767cc14\trefs/tags/26.0.10\n7dca54e8dc0dbe23995e260c989c9ef2e9023a94\trefs/tags/26.0.11\nf85a9ee99f58f644a6cee767191d46caf4bce422\trefs/tags/26.0.12\n68119d4c99b96b167da9beadb994101ff0c4a899\trefs/tags/26.0.12^{}\nba2566fe5fe30d868eceaaf209314d2ff4dcd4b6\trefs/tags/26.0.13\n64ed7c98cb97120af981b8427870d8c79ec89e4d\trefs/tags/26.0.14\n88ace656745b3e4f23e5008d5eb57fc967c8d59e\trefs/tags/26.0.15\n78fa91620bbab9271d3a6f4ad7bb804140af0096\trefs/tags/26.0.16\n25d596e2163dd8faadbc3962b0987dbb148a2bf1\trefs/tags/26.0.17\n145b72b81604490cc59ef2aedd2a35516f0e2c5f\trefs/tags/26.0.2\n52c75b843abc84c51da3c745ea00ebd81a00665a\trefs/tags/26.0.3\n436846bdf8e1dab4e9deb6c513d99fe8befdd107\trefs/tags/26.0.4\n65c34f60f4c607074c23d5572b0233f76425ce45\trefs/tags/26.0.5\n5ded193f7713fc9993c381177b70b6cbac0e92a0\trefs/tags/26.0.6\n812d849c783dbee23fcfb7266ce66321665de2bd\trefs/tags/26.0.7\n9ca0ccddbf9a69d84c8d3391a8625… [truncated]curl -sL https://github.com/keycloak/keycloak/commit/cf6e4c8be318f1e38c4001730fe6db6930dad050.patch -o /tmp/fix.patch; wc -l /tmp/fix.patch; head -100 /tmp/fix.patch{"cwd":"/workspace","duration_ms":382,"exit_code":0,"stderr":"","stdout":"155 /tmp/fix.patch\nFrom cf6e4c8be318f1e38c4001730fe6db6930dad050 Mon Sep 17 00:00:00 2001\nFrom: Ricardo Martin <rmartinc@redhat.com>\nDate: Tue, 11 Aug 2026 08:26:45 +0200\nSubject: [PATCH] Use model ID to set the\n AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED auth note (#986)\n\nCloses CVE-2026-18963\n\nSigned-off-by: rmartinc <rmartinc@redhat.com>\n---\n .../AuthenticationProcessor.java | 2 +-\n .../DefaultAuthenticationFlow.java | 12 +++---\n .../resetcred/ResetCredentialEmail.java | 9 +++-\n .../testsuite/forms/ResetPasswordTest.java | 41 +++++++++++++++++++\n 4 files changed, 57 insertions(+), 7 deletions(-)\n\ndiff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java\nindex ec149cd1f85f..f1202046a691 100755\n--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java\n+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java\n@@ -99,7 +99,7 @@ public class AuthenticationProcessor {\n public static f… [truncated]Artifacts and Evidence for CVE-2026-18963
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-18963
FAQ: CVE-2026-18963
Is CVE-2026-18963 exploitable?
How severe is CVE-2026-18963?
What type of vulnerability is CVE-2026-18963?
How can I reproduce CVE-2026-18963?
Is the CVE-2026-18963 reproduction verified?
References for CVE-2026-18963
Authoritative sources for CVE-2026-18963 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.