senn-techsenn-tech
Security
Security2026-10-09· By Franz Senn

Keycloak: 20 CVE fixes in two days, and our instance was sitting on 26.7.1

The same alert is doing the rounds again this week in videos and newsletters about self-hosted stacks: Keycloak fixed 14 CVEs, patch today rather than over the weekend. The number is correct, the framing is not. Those 14 fixes belong to release 26.7.5 from 30 September. The October release 26.8.0 from 1 October adds six more. One day sits between them, not a week. That makes 20 fixed CVEs across two consecutive days: 13 in Keycloak itself, 7 in dependencies. A second finding comes from our own operation: our Keycloak instance is running 26.7.1, inside the version range both releases close. This post is therefore also a task list for ourselves.

What the two releases actually list

The security fixes section of 26.7.5 runs to 14 entries: 10 in Keycloak itself, 4 as dependency updates (owasp-java-html-sanitizer, freemarker, bc-fips twice). 26.8.0 lists six CVEs across five entries: three in Keycloak (role escalation through an identity-provider mapper during identity brokering, unverified e-mail addresses inherited from brokered identities, group claims matched against path policies by name only), plus three dependency CVEs in two packages: two in jackson-databind 2.22.0, one in netty-codec-http 4.1.136.Final.

CVEs listed as security fixes, per release26.7.5 (2026-09-30)1426.8.0 (2026-10-01)6016
14 entries listed under Security fixes in 26.7.5 on 30 September, 6 fixed CVEs in 26.8.0 on 1 October. Counted are the entries of the Security fixes section in the official release post and release notes, dependency updates included. An upgrade from 26.7.1 to 26.8.0 reaches both batches. (source: Keycloak blog, release posts of 30 September and 1 October 2026)

For the product issues, the GitHub issues carry the note that they were migrated from a private repository. The maintainers disclosed in one go instead of letting single reports trickle out. The automated reports for the dependency packages do not carry that note. Between 26.7.4 and 26.8.0 sits a real security package, not a maintenance release with a few CVEs on the side.

The ten product issues from 26.7.5 in short form. All ten scores come from Red Hat and sit there with status draft, so they are not final. The NVD disagrees on two: CVE-2026-18211 at 5.4 instead of 4.2, CVE-2026-18217 at 4.7 instead of 3.4.

CVEIn shortRed Hat
CVE-2026-18203Group policy matches prefixes of sibling pathsModerate 6.5
CVE-2026-18207Source-group condition hits duplicate group names, negative logic can missModerate 6.5
CVE-2026-18208Introspection responses for foreign audiences can carry a signed JWT claimModerate 6.5
CVE-2026-88770Device grant issues tokens for locked accountsModerate 6.5
CVE-2026-89298Client secret in clear text to read-only access (view-clients)Moderate 4.9
CVE-2026-16103Brute-force lockout not checked during CIBA token redemptionModerate 4.3
CVE-2026-93999Refresh keeps issuing tokens for a disabled clientModerate 4.2
CVE-2026-18211The localhost exception in secure-client-uris swallows localhost prefix domainsModerate 4.2
CVE-2026-18217SAML redirect binding: parameter pollution can land a login in the wrong accountModerate 3.4
CVE-2026-18206Client policy wildcard matches non-subdomain suffixesLow 3.7

Then the four dependency updates in the same section: owasp-java-html-sanitizer (CVE-2025-66021, XSS), freemarker (CVE-2026-84939, path traversal through a malformed locale), bc-fips twice (CVE-2026-8798, CVE-2026-13505). Red Hat has no entry for CVE-2026-8798 at all. That one comes from Bouncy Castle itself and sits in the CPU entropy source; MITRE lists it as published, CVSS v4.0 at 8.7.

The three gaps every operator should know

A disabled client that still gets tokens. CVE-2026-93999 (Moderate, CVSS 4.2 according to Red Hat, score still draft): a client is disabled in the admin console, every path checks the disablement during the actual exchange except the refresh path. Hold an older refresh token and you keep receiving freshly signed access tokens whose aud field names the disabled client. That includes refresh tokens issued through token exchange; the immediate exchange path, by contrast, answers invalid_client. Reproduced on 26.7.2. Resource servers that introspect online at the authorization server per request notice nothing. The ones that verify JWTs offline against the JWKS are validating a correctly signed token from a client that should no longer exist. Reach follows the roles of the user in question, so this is not automatically a privilege gain. It still belongs in every cleanup routine: disabling a client was not a reliable revocation signal.

Read-only admins see client secrets. CVE-2026-89298 (Moderate, 4.9): through the client registration API, a plain GET returned the confidential client secret in clear text to accounts holding the view-clients role, meaning pure read access. A read-only right over the client list becomes the starting point for an escalation path. Authentication is still required, but the bar sits lower than for most earlier admin API mistakes. In 26.8.0 the same subject area appears a second time as a documented weakness, "Client GET endpoints return raw client secrets to view-clients role holders", in a separate Weaknesses section. That section lists 96 entries without a CVE number, roughly twenty of them security related, the rest tests and housekeeping.

An account lockout that does not lock out. CVE-2026-88770 (Moderate, 6.5) and CVE-2026-16103 (Moderate, 4.3): in the device authorization grant and in CIBA token redemption, the check for a brute-force locked account was missing. The CIBA issue is unfinished follow-up work. The lockout had already been added at initiation, and forgotten again at token redemption. Anyone able to drive the grant flow received tokens for accounts that were locked at that moment for repeated sign-in attempts. The attacker has to be able to start the flow, which is a real condition. It does defeat exactly the mechanism that is supposed to stop them.

Then the small and sharp ones. The localhost exception in secure-client-uris accepted attacker domains starting with localhost. A wildcard match in client policies matched non-subdomain suffixes, so attackerexample.com passes against *.example.com. SAML redirect binding could be pushed by parameter pollution into a login at the wrong account, provided the redirect URL was opened wide with a wildcard.

Why the alert with the wrong date circulates, and why that matters

The project itself has published no security advisory of its own since GHSA-xpwp-2pcm-8xq3 for CVE-2026-90997 on 16 September, as of 9 October. For individual CVEs among the 20 there are probably entries in the GitHub advisory database, GHSA-82wc-7jr6-wxgm for CVE-2026-93999 from 19 September being one. Those are unreviewed imports of the NVD entry, not statements from the maintainers. The scores for the 20 fixes therefore come from the Red Hat tracker for now, and they sit there on draft. Red Hat's build of Keycloak is listed as not affected for CVE-2026-90997, and there is no RHSA. Anyone quoting a Keycloak vulnerability should check which kind of source set the number: release note, tracker or video.

The only High in this window is CVE-2026-90997 (CVSS 7.4, fixed in 26.7.4). In stateless operation on MySQL or MariaDB, a mismatch in the number of rows checked lets single-use delegation tokens, client JWT assertions, DPoP proofs and TOTP codes, be accepted a second time. Reachable without authentication, and the advisory's workaround is to turn the stateless feature off. Red Hat additionally names the JDBC parameter useAffectedRows=true as a mitigation. Anyone running 26.7.0 to 26.7.3 on MySQL or MariaDB with stateless enabled has the reason to patch immediately right there.

For completeness: a backport to the 26.6 line, "26.6.8", shows up in issue labels and does not exist as a release. Anyone still on 26.6.x is waiting for nothing; the way is to 26.7.5 or 26.8.0 directly.

Our status, measured on 9 October 2026

Our instance runs as the container quay.io/keycloak/keycloak:26.7.1 on a dedicated host, the database as keycloak-db on postgres:17-alpine. Measured with docker inspect against the image tag; the compose file carries the pinned form with tag and digest, sha256:f1f1f01e…c01c6. The configuration sets KC_DB=postgres and a JDBC URL pointing at that Postgres. No feature flag for stateless operation is set, the function has to be activated explicitly. So the precondition of CVE-2026-90997, stateless on MySQL or MariaDB, is not met on our side. The container has been running since 7 October without a restart. How this instance sits in front of our internal services as single sign-on is described here.

Our release overview for week 39 already listed 26.7.4 as an open item, together with the note that six CVEs stay unpatched if we are still on 26.7.3 or older. What that overview did not do is measure the version we actually run, which was a gap in our routine rather than in the source. Today's measurement closes it: at 26.7.1 we sit behind 26.7.4, so behind the fix for CVE-2026-90997 as well, and in front of all 20 fixes from the two new releases.

Moving to 26.8.0 is not a bare compose pull on our side. The host bind-mounts the truststore holding the Samba AD certificate read-only into the container's truststore directory, and a plain pull would quietly break sign-in through Active Directory, at the latest on the next contact. A dedicated script on the host therefore restarts the identity services in the right order, and it can be rehearsed in dry-run mode first. Image swap, database backup beforehand, truststore check afterwards. No maintenance window is set for it yet. That is the task this post leaves behind for ourselves.

What operators do now

  1. Measure the version first: docker inspect --format '{{.Config.Image}}' on the Keycloak container. The compose file carries the pinned digest, and docker images --digests lists it too.
  2. Everything below 26.7.5 is affected by at least one of the two batches. The target is 26.8.0, the version one day later.
  3. If you run 26.7.0 to 26.7.3 on MySQL or MariaDB with the stateless function: patch now or switch stateless off.
  4. After the upgrade: check which resource servers verify JWTs offline instead of introspecting online. Until this fix, a disabled client was not a revocation for those systems.
  5. Clean up admin roles: before 26.7.5, view-clients was more than a read right.

Further reading

Questions?
Is the alert right that Keycloak fixed 14 CVEs in early October?+

The 14 fixes are real, the date is not. They ship in version 26.7.5 from 30 September 2026. The October release 26.8.0 from 1 October lists six further security fixes. Nowhere is it written that 26.8.0 contains everything from 26.7.5. The fix issues from 26.7.5 do carry the 26.8.0 label on top, and 26.8.0 is the higher version, so going from 26.7.1 straight to 26.8.0 picks up both batches. Checking your own version takes two minutes, so do that first.

Which of these is the awkward one for a normal self-hosted setup?+

CVE-2026-93999: after you disable a client in the admin console, token refresh keeps issuing valid access tokens for it, because the refresh path never checks isEnabled. Resource servers that introspect online at the authorization server are unaffected, the ones that verify JWTs offline against the signing keys are not. Then CVE-2026-89298: plain read access to the clients, the view-clients role, returned client secrets in clear text through the client registration API.

Do I have to restart my production SSO right now?+

Most of the 14 fixes are Moderate issues that need authentication or a specific configuration. The operational core is an upgrade window: at minimum 26.7.5, better 26.8.0. If you are stuck on 26.7.0 to 26.7.3 and run stateless on MySQL or MariaDB, do not wait. That is where CVE-2026-90997 sits, the only High in the window, 7.4 without authentication, and Red Hat carries the same number under Important.