Two Keycloak 26.8.1 0-Days: Exposure Triage, Threat Watch, and a Lab Reproduction
Nov 4, 2026
Two fresh Keycloak 26.8.1 0-days with public PoCs. How we triaged our own realm's exposure, added both to our public Threat Watch feed, and reproduced the manage-users privesc against a stock 26.8.1 container.
On 3 November 2026, Abraxas Labs published two lab-proven 0-days against Keycloak 26.8.1 (commit 24ed1dad, the current GA), each with a working proof of concept on GitHub. Neither has a CVE or a vendor patch yet. One is an unauthenticated SAML account-takeover; the other is an authenticated privilege escalation from a help-desk admin to a full realm read.
This post is what we did about them, in the order that mattered:
- Got them into our public Threat Watch feed so they are tracked (and so the feed's readers get the preconditions straight — both bugs are heavily configuration-dependent, and "affected" oversells them without that context).
- Triaged our own exposure — Helix's SSO runs on Keycloak, so we checked both preconditions against our production realm from the outside.
- Reproduced the privesc end-to-end against a stock
quay.io/keycloak/keycloak:26.8.1container in a loopback lab, negative control included.
Everything below was verified against the primary sources on 4 November 2026: the Abraxas Labs write-ups, the two PoC repositories, and our own lab run.
The two bugs in one table
| SAML redirect query-name split | manage-users composite mapping | |
|---|---|---|
| Reach | Unauthenticated to Keycloak | Authenticated (manage-users) |
| Severity | High 7.4 (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N) | Medium 6.6 (CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H) |
| CWE | 287, 347, 116 | 269, 266 |
| Precondition | A SAML identity-provider broker that signs HTTP-Redirect responses, with validateSignature=true and wantAssertionsSigned absent or false | A pre-built composite role that quietly includes manage-realm or realm-admin, plus an account holding manage-users |
| Effect | Finish someone else's brokered SAML login as them | Delegated admin reads the full realm document |
| PoC | keycloak-saml-redirect-param-split | keycloak-manage-users-composite |
| CVE / patch | None yet | None yet |
Both are "leftover" bugs in the researcher's taxonomy: a nearby path that a recent fix taught to check composites or decode names, while a sibling path still doesn't. Neither is host RCE, and both close cleanly under stated conditions.
Bug 1: the forged document rides in front of an honest signature
The SAML broker endpoint takes the response with @QueryParam("SAMLResponse"). RESTEasy Reactive injects that through Vert.x, which percent-decodes query names — so SAML%52esponse (%52 is R) collapses onto SAMLResponse, and the first value, the attacker's forged document, wins. The redirect-signature check then rebuilds the signed string from the raw query (KeycloakUriInfo.getQueryParameters(false)), where SAML%52esponse is still a distinct key. The IdP's genuine SAMLResponse, RelayState and SigAlg are what get verified — and the signature is honest. Keycloak logs the attacker in as the NameID of the forged document.
The preconditions are doing a lot of work here:
- The realm needs a SAML broker (Google, GitHub, OIDC brokers don't qualify).
- The IdP must sign HTTP-Redirect responses — a POST-only IdP never hands the attacker a GET signature to reuse.
validateSignature=true(so the honest signature is checked) andwantAssertionsSignedabsent/false — which is exactly what the metadata import path configures, because a signing certificate turns validation on but never sets the assertion flag.
The researcher's own "stays closed" table says it plainly: no SAML broker, no attack path. That line is the whole exposure question for most deployments.
Bug 2: canMapRole doesn't walk composites
Fine-grained admin permissions v2 has been the default evaluator since it shipped; you don't have to opt in. Its RolePermissionsV2.canMapRole returns true for any role whose own name is not an admin role — it never inspects what the role contains. CVE-2026-94000 taught the group-join path to walk composites (GroupUtils.checkAdminGroupRoles, and the same walk on UsersResource.createUser). The role-mapping POST never learned the trick.
So a help-desk account holding only realm-management/manage-users can POST a non-admin composite that includes manage-realm onto itself:
POST /admin/realms/{realm}/users/{id}/role-mappings/realm → 204
GET /admin/realms/{realm} → full document
On the very next request, UserAdapter.hasRole walks composites, and the same access token evaluates as holding manage-realm. The realm document flips from the partial view (manage-users is itself an admin role, so it gets a 200 with a subset of fields) to the full document — attributes, accessTokenLifespan, everything.
Two honest limits, both in the write-up: the attacker cannot create that composite (canMapComposite still calls checkAdminRoles), and direct assignment of a role named manage-realm or realm-admin is still refused. A real realm administrator must have built the leftover composite first. Help-desk realms collect those.
Step 1: both went into Threat Watch
We run a public incident feed at helix.ml/threat-watch; tracking a no-CVE 0-day with public PoCs is exactly what it is for. Both records are live, marked unpatched with PoC public:
- Keycloak: account takeover through SAML broker redirect-parameter split
- Keycloak: manage-users admin escalates to manage-realm via composite role mapping
The records carry the preconditions front and centre, because the difference between "Keycloak vulnerable" and "your realm exposed" is entirely configuration.
Step 2: checking our own realm from the outside
Helix's SSO is Keycloak. You can do this whole check against any Keycloak deployment with nothing but curl, and we recommend it:
Find the Keycloak. Follow your product's login flow to the OIDC auth URL. Ours resolves to https://app.helix.ml/auth/, realm helix — Keycloak fronted by the same domain.
List the identity providers. Fetch the login page and collect every broker/* link:
GET /auth/realms/{realm}/protocol/openid-connect/auth?client_id=...
The login page renders one button per enabled broker. Ours has exactly one: broker/google — Google, via OIDC. No SAML broker means bug 1 has no path, full stop. (The realm's own SAML SP descriptor at /protocol/saml/descriptor returning 200 is normal and unrelated — that is Keycloak acting as an SP for its clients, not a broker.)
Assume bug 2 is reachable unless the realm is clean. The privesc needs a manage-users account and a leftover composite containing manage-realm or realm-admin. Both are internal configuration facts — from the outside, an attacker (or an auditor) can only see that the admin REST API exists. Ours sits behind Cloudflare Access, which is the correct posture, but the real question is answered in the admin console: Realm roles → composites, and the list of accounts holding manage-users. If the answer is "none and few", the bug is theoretical for your realm.
Note what you cannot see. The Keycloak version is not readable without authentication — the resource URLs use a content hash, and /auth/admin/serverinfo requires an admin token. For us that check happens inside the deployment, where it belongs.
One hygiene finding from the same pass: our master realm's .well-known/openid-configuration answers publicly. That is Keycloak's default and not one of these two bugs, but most hardening guides prefer master unreachable — we've noted it for review.
Step 3: the reproduction
We ran the researcher's lab for the composite bug against a stock container: quay.io/keycloak/keycloak:26.8.1 under Docker, bound to 127.0.0.1:18082 only, start-dev, bootstrap admin. No external traffic, no real identity providers. The lab verifies the version twice (image label and live serverinfo), then builds the scenario the write-up describes:
- A realm carrying a planted witness attribute (
kcr02m7q4) that only appears in the full realm document. - A role
sneaky-compositewhose only composite isrealm-management/manage-realm— created by the realm admin, as the precondition requires. - Two users holding exactly
manage-users: the attacker and a control who never self-maps.
Then the attack, verbatim from the run:
IOC image=quay.io/keycloak/keycloak:26.8.1 label-version=26.8.1
IOC serverinfo-version=26.8.1
IOC admin-composite=manage-realm
IOC limited-direct=manage-users
IOC control-direct=manage-users
IOC before-status=200 witness=0 full=0
IOC available=sneaky-composite
IOC post-status=204
IOC after-get attempt=1 status=200 witness=1 full=1
IOC control-status=200 witness=0 full=0 same-token=1
SUCCESS KC-H02-ROLEMAP kcr02m7q4
Reading the trace:
before-status=200 witness=0 full=0— themanage-usersaccount GETs/admin/realms/laband gets a partial document. HTTP 200, not 403:manage-usersis itself an admin role, so the server answers, just with the fields the role can't see withheld.post-status=204— the self-mapping POST is accepted. This is the leftover:canMapRolesaw a role namedsneaky-composite, which is not an admin role's name, and granted it.after-get attempt=1 status=200 witness=1 full=1— immediately, on the same access token, the realm document is complete. Witness attribute,accessTokenLifespan, the lot. The grant took effect on the very next request.control-status=200 witness=0 full=0— the secondmanage-usersuser, who did not self-map, still sees the partial document on the same build. The composite mapping is what flipped visibility; the version and configuration alone didn't.
One client-side note for anyone rerunning the PoC: the released poc.py wraps builtins.print with a function that then calls builtins.print — i.e., itself — so the first coloured line dies with a RecursionError before the lab does anything. We patched our local copy's print plumbing; the vulnerability logic is untouched.
What to do while there is no patch
- Inventory your SAML brokers (Admin console → Identity providers). No SAML broker: bug 1 cannot touch you.
- For redirect-signing SAML brokers, set
wantAssertionsSigned=true— and recheck brokers created by metadata import, which leaves it off. Where possible, prefer POST binding on the IdP side. - Audit realm and client roles for composites containing admin roles (
manage-realm,realm-admin,manage-clients, ...) and delete any that aren't meant to grant admin. Until a fix ships, treat everymanage-usersholder as able to reach any admin power sitting inside such a composite. - Keep the
manage-userslist short. The delegated-admin pattern is the attack surface here. - Hunt in admin events: role-mapping CREATE operations where the target user is the acting admin (
resourcePathlikeusers/<own-id>/role-mappings/...) are worth a look. - Watch keycloak.org/security for the CVE and fixed release, and upgrade when one lands.
- Is my Keycloak affected by these two 0-days?
- It depends on configuration more than version. The SAML bug needs a SAML identity-provider broker that signs HTTP-Redirect responses with wantAssertionsSigned off — the configuration metadata import produces. The privesc needs an account with manage-users plus a pre-built composite role containing manage-realm or realm-admin. Check both; neither applies to a realm that fails its precondition.
- Is there a patch yet?
- Not as of 4 November 2026. Both bugs are published with working PoCs but no CVE and no fixed release. The mitigations above close both preconditions: wantAssertionsSigned=true for redirect-signing SAML brokers, and removing composites that contain admin roles.
- How did Helix verify this?
- We ran the researcher's lab unchanged against a stock quay.io/keycloak/keycloak:26.8.1 container on loopback: version verified from the image label and the live serverinfo endpoint, a planted witness attribute proving the partial-to-full document flip, and a negative control user who stays partial. The only change was a one-line fix to a print-wrapper bug in the PoC client itself.