Keycloak 26.8.1, manage-users maps a composite admin role
Keycloak 26.8.1 RolePermissionsV2.canMapRole does not walk composites. A manage-users admin can map a pre-built non-admin composite that includes manage-realm onto himself and then read the full realm document. Sibling of CVE-2026-94000 on the role-mapping POST. Labbed. No CVE yet.
- Name
- Keycloak 26.8.1, manage-users maps a composite admin role
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- medium
- Disclosure Status
- public
- Vendor
- Red Hat
- Affected
- Keycloak 26.8.1 (24ed1dad). Image quay.io/keycloak/keycloak:26.8.1. Needs manage-users plus a pre-existing composite of manage-realm or realm-admin. FGAP v2 is the default evaluator.
- Published
- 03 Nov 2026
- Updated
- 03 Nov 2026
- Tags
- 0-day, keycloak, auth, privilege-leftover, cwe-269, cwe-266, authenticated
The job title was not supposed to include the keys.
I am @abraxas_null. The proof of concept is on GitHub: abraxas/keycloak-manage-users-composite (loopback client). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh, poc.py. Authorized lab only. It talks to loopback.
This is Keycloak 26.8.1 (24ed1dadf3f246a555adb6d3d4a99e9b3003fb3b). No CVE yet. CWE-269 / CWE-266. 6.6 Medium (CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H). Remote auth leftover. Authenticated Admin REST. Same shape as CVE-2026-94000.
A help-desk account that is only supposed to reset passwords and create users can promote itself. Someone with Keycloak's manage-users role finds a leftover job-title in the realm (a composite role that quietly includes manage-realm), pins that title on their own account, and then reads the full realm document that was hidden from them a request earlier. They cannot mint that composite. A real realm administrator has to have built it first.
This is not anonymous. This is not host RCE. Direct assignment of a role named manage-realm is still refused.
Why this leftover, after that CVE
CVE-2026-94000 taught Keycloak to walk composites on group join. GroupUtils.checkAdminGroupRoles looks at nested admin roles before a manage-users admin can join a group that carries them. UsersResource.createUser does the same. The role-mapping POST does not call that walk.
I sat on RoleMapperResource.addRealmRoleMappings because that is the other way you attach a role to a user. If the group path learned to walk and the mapping path did not, the leftover is the mapping path.
Pinning 26.8.1
Same pin as the SAML leftover: 26.8.1, commit 24ed1dad, image quay.io/keycloak/keycloak:26.8.1. Fine-grained admin permissions v2 is Type.DEFAULT. Profile.isEnabledByDefault turns that type on. AdminPermissions.evaluator then uses MgmtPermissionsV2, whose roles() constructs RolePermissionsV2. You do not have to turn FGAP on. A default install is already on v2.
The skip list
Same public 26.8.1 CVE and weakness batch as the SAML post. I did not refile CVE-2026-94000. I did not refile #53417 (token role stripping). I did not treat realm-admin doing realm-admin work as a leftover.
canMapRole does not walk
RolePermissionsV2.canMapRole returns true for every role that fails isRealmAdminRole. It never walks composites. UserPermissionsV2.canMapRoles is true for manage-users. RoleMapperResource.addRealmRoleMappings then grantRole. The same check is ClientRoleMappingsResource.addClientRoleMapping.
The next admin request treats the caller as holding the nested admin role. UserAdapter.hasRole walks composites. security-admin-console and admin-cli are created with full scope, so ClientAdapter.hasScope is true and KeycloakIdentity.hasClientRole trusts the user model.
He cannot create that composite himself. canMapComposite on an admin role still calls checkAdminRoles. Direct assignment of a role whose name is in AdminRoles.ALL_ROLES on realm-management is still refused. RolePermissionsV2.isRealmAdminRole is wider than the v1 helper. I wasted a pass on v1. v2 overrides it.
A stock realm does not ship the composite. Someone with real manage-realm has to have created it, the same setup the CVE-2026-94000 group test builds. Help-desk realms collect leftover titles. That is the precondition.
What an attacker can do
Needs an existing Keycloak account that already holds manage-users, and a realm or client role whose own name is not an admin role and whose composites include manage-realm or realm-admin.
| Privilege | Method / sink | Recon / integrity / availability | Lab-proven or inferred |
|---|---|---|---|
| Map a non-admin composite onto self | POST /admin/realms/{realm}/users/{id}/role-mappings/realm |
Integrity (role grant) | Lab-proven. HTTP 204. Witness kcr02m7q4 |
| Same grant on a client role | POST /admin/realms/{realm}/users/{id}/role-mappings/clients/{client} |
Integrity | Source. Same canMapRole |
| Read the full realm document on the same access token | GET /admin/realms/{realm} after the mapping |
Recon | Lab-proven. Before: HTTP 200 partial (no attributes, no accessTokenLifespan). After: HTTP 200 full, planted attribute present |
Use nested manage-realm on later admin calls |
UserAdapter.hasRole walks composites |
Integrity | Inferred from source. Lab oracle is the full realm GET |
Nested realm-admin when that is the composite |
Same walk | Integrity | Inferred. Lab used manage-realm |
| Negative control | A second manage-users user who does not map the composite |
n/a | Lab-proven. Control GET stays partial |
Stays closed on this leftover
| Axis | Method / sink | Why it is closed |
|---|---|---|
| Integrity | Direct map of manage-realm / realm-admin |
isRealmAdminRole still refuses |
| Integrity | Create the composite that includes an admin role | canMapComposite still calls checkAdminRoles |
| Integrity | Join a group whose composite includes an admin role | CVE-2026-94000 |
| Integrity | Create a user already in that group | Same walk on UsersResource.createUser |
| Recon | Anonymous Admin REST | Needs a manage-users token |
| Integrity | Host RCE | Out of class |
GET /admin/realms/{realm} is not HTTP 403 for manage-users. That role is itself an admin role, so the server returns 200 with a partial document. I expected 403. Live result is the partial 200. The leftover is the fields that appear only after the composite is mapped: attributes, accessTokenLifespan, the planted witness.
The lab
Stock quay.io/keycloak/keycloak:26.8.1 on 127.0.0.1:18082. Bootstrap admin creates realm lab with a planted attribute, creates sneaky-composite whose only composite is realm-management / manage-realm, and grants two users only manage-users. The limited user POSTs the composite onto himself. HTTP 204. The same access token then reads the full realm document. The control user stays on the partial document. Image label and serverinfo are both 26.8.1.
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 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 kcr02m7q4Wrong turns already recorded: treating v1 isRealmAdminRole as the live check; expecting HTTP 403 on the realm GET before the grant; re-filing CVE-2026-94000; a reverse shell. Theatre.
The fix
Walk composites in RolePermissionsV2.canMapRole the way GroupUtils.checkAdminGroupRoles already does for group join. A role that includes manage-realm or realm-admin is an admin role even when its own name is not.
Until then, do not leave a composite of manage-realm sitting in a realm that has help-desk manage-users accounts. The group path learned. The mapping path did not.
I am @abraxas_null. Site abraxaslabs.tech. GitHub abraxas. Mail abraxas.null@proton.me.