Skip to content

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.

REPRO-2026-00337 Aug 24, 2026 CVE entry .txt
Severity
CRITICAL
Confidence
HIGH
Reproduced in
0m 17s
Tool calls
228
Spend
$0.03
01 · Overview

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).

02 · Severity & CVSS

CVE-2026-18963 Severity

CVE-2026-18963 is rated critical severity.

CRITICAL threat level
Weakness CWE-640 Weak Password Recovery Mechanism for Forgotten Password

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
or 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
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-18963

Authorization bypass — reproduced
  • 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
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

Attack chain
  1. GET/POST /realms/cvetest/login-actions/reset-credentials (Keycloak reset-credentials flow; ResetCredentialEmail execution)
How the agent worked 521 events · 228 tool calls · 5h 13m
5h 13mDuration
228Tool calls
95Reasoning steps
521Events
35Dead-ends
Agent activity over 5h 13m
Policy
3
Support
12
Repro
303
Judge
30
Variant
164
Verify
3
0:00312:30

Root Cause and Exploit Chain for CVE-2026-18963

Versions: Keycloak upstream < 26.4.15, 26.5.x, 26.6.0–26.6.5, 26.7.0–26.7.1

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 (services module: 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):

  1. When the user clicks "try another way", processAction() stores AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED = "true" (a boolean), not bound to any execution.
  2. processFlow() checks only Boolean.parseBoolean(note) plus a non-null CURRENT_AUTHENTICATION_EXECUTION note. After the attacker posts the victim's username, ResetCredentialEmail.authenticate() sends the reset email and calls context.forkWithSuccessMessage(EMAIL_SENT); the FORK handling sets CURRENT_AUTHENTICATION_EXECUTION to the email execution id. A subsequent GET refresh of the original reset-credentials URL therefore re-renders createSelectAuthenticatorsScreen(emailExecution), whose HTML form action leaks the email execution UUID (execution=<uuid>).
  3. ResetCredentialEmail.action() (vulnerable) is:
    public void action(AuthenticationFlowContext context) {
        context.getUser().setEmailVerified(true);
        context.success();
    }
    
    Posting to the leaked email-execution URL (no action token required) invokes this blind success, the required-elements chain continues to reset-password / UPDATE_PASSWORD, and the attacker sets a new password on the victim identity attached to the authentication session by ResetCredentialChooseUser.

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 matches CURRENT_AUTHENTICATION_EXECUTION (otherwise the note is removed) — the GET refresh after the email fork no longer re-renders the selector screen.
  • ResetCredentialEmail.action() now requires user.getId().equals(authSession.getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID)), i.e. success is only possible through the emailed action token; otherwise the flow fails with INVALID_USER.

The upstream regression test ResetPasswordTest.resetPasswordTryAnotherWay() served as the PoC blueprint for the flow-selector re-entry.

Reproduction Steps

  1. bundle/repro/reproduction_steps.sh (self-contained; helper bundle/repro/smtp_sink.py is a promoted required artifact).
  2. 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), user victim (email victim@cvetest.local, password OldPass123!), and a public direct-grant client atk-cli used only for verification.
    • Runs the full browser-equivalent attack with curl (cookie jar, per-page rotating session_code honored): login page → forgot-password → POST tryAnotherWay=on → POST username=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 returns 302 → login-actions/required-action?execution=UPDATE_PASSWORD, step-7 returns 302 → /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.json on every exit (trap) and streams all output to bundle/logs/reproduction_steps.log.
  3. Expected evidence: 2/2 vulnerable attempts EXPLOITED, 2/2 fixed 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=0 and the re-rendered selector form action now contains execution=0089b76d-... (the email execution, different from the choose-user execution 141b34d5-... 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 blind context.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 as victim.
    • step8 takeover verify: NEW password token HTTP 200, OLD password token HTTP 400.
  • bundle/logs/fixed-attempt-{1,2}.logstep5 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 addressed To: victim@cvetest.local containing 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-version reported by /admin/serverinfo was 26.7.1 / 26.7.2 respectively.
  • bundle/repro/runtime_manifest.json — runtime identity (image digests quay.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), hostname http://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_ID note in ResetCredentialEmail.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.

Event 1/40
0:002:02
0:00
session startedaccounts/fireworks/models/kimi-k3 · CVE-2026-18963 · REPRO-20
0:08
0:10
web search
0:11
web search
0:27
0:29
web search
1:15
1:15
extract_facts
no facts extracted
1:19
1:19
supportclaim_contract
1:26
1:26
1:26
1:31
1:31
1:35
1:35
1:35
1:38
1:38
1:45
1:51
$docker --version 2>&1; echo "---"; docker info 2>&1 | head -20; echo "---"; java -version 2>&1; echo "---"; docker images 2>&1 | head -20
0.5s
Docker 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.)
1:53
$git ls-remote https://github.com/keycloak/keycloak.git 2>&1 | grep -E "cf6e4c8|refs/tags/26\.[0-9]" | head -30; echo "EXIT: $?"
2.0s
{"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]
1:59
2:00
$curl -sL https://github.com/keycloak/keycloak/commit/cf6e4c8be318f1e38c4001730fe6db6930dad050.patch -o /tmp/fix.patch; wc -l /tmp/fix.patch; head -100 /tmp/fix.patch
0.7s
{"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]
2:02
08 · How to Fix

How to Fix CVE-2026-18963

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-18963 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-18963

Is CVE-2026-18963 exploitable?

Yes. Pruva independently reproduced CVE-2026-18963 and verified the exploit fires end-to-end in a sandboxed environment. A runnable proof-of-concept script and the full agent transcript are on this page (reproduction REPRO-2026-00337).

How severe is CVE-2026-18963?

CVE-2026-18963 is rated critical severity.

What type of vulnerability is CVE-2026-18963?

CVE-2026-18963 is classified as CWE-640 Weak Password Recovery Mechanism for Forgotten Password.

How can I reproduce CVE-2026-18963?

Pruva provides a verified reproduction script on this page. Download it and run it inside an isolated environment such as a container or virtual machine — never against production. The reproduction was confirmed end-to-end by Pruva's automated agents.

Is the CVE-2026-18963 reproduction verified?

Yes. Pruva reproduced CVE-2026-18963 with high confidence in a sandboxed environment, capturing the full agent transcript and artifacts as evidence.
11 · References

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.