8000 fix(admin): block TOCTOU rename bypass of protected admin roles by ThreeMangoTrees · Pull Request #49516 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

fix(admin): block TOCTOU rename bypass of protected admin roles - #49516

Closed
ThreeMangoTrees wants to merge 1 commit into
keycloak:mainfrom
ThreeMangoTrees:threemangotrees/issue-49427
Closed

ThreeMangoTrees wants to merge 1 commit into
keycloak:mainfrom
ThreeMangoTrees:threemangotrees/issue-49427

Conversation

@ThreeMangoTrees
Copy link
Copy Markdown
Contributor

Summary

Closes #49427

A delegated admin holding only manage-clients could exploit two gaps in role-rename validation to escalate to full realm-admin privileges.

Attack 1 — TOCTOU via rename-away

The checkAdminRoles composite guard is name-based: it checks role.getName() against AdminRoles.ALL_ROLES. Before this fix, no guard ran during a rename, enabling this sequence:

  1. Rename realm-admin → temp-role — the name leaves ALL_ROLES; the composite guard goes blind.
  2. Add temp-role as a composite child of an attacker-controlled role.
  3. Rename temp-role → realm-admin — composites are stored by role ID, so the link persists.

Any user assigned the attacker-controlled role now transitively holds full realm-admin privileges.

Attack 2 — Rename-to bypass

A delegated admin creates an innocent role on their own client and renames it to "realm-admin". That role now passes name-based guards even though it was never a real admin role.


Changes

RoleResource — new isProtectedAdminRoleRename(role, newName)

Core helper shared by both update endpoints. Two-tier logic:

  • Rename TO a protected name (newName ∈ ALL_ROLES): blocked unconditionally for any container. Stops Attack 2.
  • Rename FROM a protected name (role.getName() ∈ ALL_ROLES): blocked only when the role belongs to an admin container (the realm itself, or the realm-management / master-realm client). Container scoping avoids false positives for custom client roles whose names coincidentally collide with an admin role string.

RoleByIdResource — PUT /roles-by-id/{id}

Guard applied after requireManage() and before updateRole(). A delegated admin that passes requireManage but lacks canManageRealm() is rejected with 403 Forbidden.

RoleContainerResource — PUT /roles/{name} and PUT /clients/{id}/roles/{name} (additional fix)

This endpoint was entirely unguarded. Patching only RoleByIdResource would leave the vulnerability trivially bypassable via the named-role URL. The same guard is applied here to deliver a complete remediation.

RoleByIdResourceTest — 4 new integration tests

Each test creates a delegated admin with only manage-clients and verifies a specific attack vector is blocked:

Test Scenario
testRenameProtectedRoleBlockedForDelegatedAdmin Cannot rename realm-admin → temp-role via ID endpoint
testRenamingToProtectedRoleNameBlockedForDelegatedAdmin Cannot rename an innocent custom client role → realm-admin via ID endpoint
testRenameProtectedRoleBlockedViaNamedRoleEndpoint Cannot rename realm-admin → temp-role via named-role endpoint
testTOCTOURenameCompositeAttackPrevented Full TOCTOU sequence blocked at step 1; realm-admin name confirmed unchanged

Test plan

  • All four new tests in RoleByIdResourceTest pass
  • Existing role management tests in RoleByIdResourceTest and RoleContainerResourceTest are unaffected
  • A full realm admin (with manage-realm) can still rename admin roles normally

A delegated admin with manage-clients could exploit two gaps:

1. TOCTOU via ID-based rename: rename realm-admin → temp-role (name
   leaves ALL_ROLES, composite guard goes blind), add temp-role as a
   composite of an attacker-controlled role, then rename temp-role →
   realm-admin. Any user assigned the attacker-controlled role now
   transitively holds full realm-admin privileges.

2. Rename-to bypass: rename an innocent role on any custom client to a
   protected name (e.g. realm-admin), bypassing name-based guards
   regardless of the role's container.

Changes:

  RoleResource (base class)
  - Add isProtectedAdminRoleRename(role, newName): returns true when the
    target name is in AdminRoles.ALL_ROLES (catches rename-to attack), or
    when the current name is in ALL_ROLES and the role belongs to an admin
    container (realm-management / master-realm client, or the realm itself).
    Container scoping avoids false positives for unrelated custom client roles.

  RoleByIdResource.updateRole  (PUT /roles-by-id/{id})
  - Apply isProtectedAdminRoleRename guard; require canManageRealm().
  - Remove redundant canManage() check (already enforced by requireManage()).

  RoleContainerResource.updateRole  (PUT /roles/{name},
                                     PUT /clients/{id}/roles/{name})
  - Apply the same guard. This endpoint was entirely unguarded and
    provided a complete bypass of the fix in RoleByIdResource.

  RoleByIdResourceTest
  - Four new integration tests: delegated admin (manage-clients only) is
    blocked from renaming realm-admin away from its protected name via the
    ID-based endpoint; blocked from renaming any role to a protected name;
    blocked via the named-role endpoint; full TOCTOU sequence is stopped at
    step 1 and realm-admin name is confirmed unchanged.

Closes keycloak#49427

Signed-off-by: Vinit Kumar <30852363+ThreeMangoTrees@users.noreply.github.com>
@ThreeMangoTrees
Copy link
Copy Markdown
Contributor Author

@abstractj can you review this PR?

@abstractj
Copy link
Copy Markdown
Contributor

That's for @keycloak/core-iam maintainers.

if (role == null) {
throw new NotFoundException("Could not find role");
}
if (isProtectedAdminRoleRename(role, rep.getName()) && !auth.realm().canManageRealm()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also related to this: Looking at the resources (both RoleByIdResource and RoleContainerResource, the admin roles are still vulnerable to deletes - shouldn't we prevent deletion of protected roles as well?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, IMO we should not allow deletion as well.

* full realm-management permission. Renaming TO a protected name is blocked for any
* container; renaming FROM one is only blocked for admin containers (realm or realm-management client).
*/
protected boolean isProtectedAdminRoleRename(RoleModel role, String newName) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm getting this right, it would block other updates for protected roles even if we're not renaming the role. For example, if one changes the role description this method would block the update even if no rename is happening.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that is the goal.

@sguilhen
sguilhen requested a review from pedroigor June 2, 2026 16:52
@pedroigor pedroigor left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @ThreeMangoTrees.

It would be better if we introduce the "rename" check to org.keycloak.services.resources.admin.fgap.RolePermissions so that we just call this method from both endpoints you are updating here.

It will also make it easier to implement isProtectedAdminRoleRename as you will be able to reuse some code from there that handles the "master realm-management clients" (e.g.: master-realm, foo-realm).

if (role.getContainer() instanceof RealmModel) {
return true;
}
if (role.getContainer() instanceof ClientModel client) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to check the client roles from master realm clients such as foo-realm and master-realm. See AdminRoles.APP_SUFFIX.

This is the "realm-management" client, but in the master realm, which allows cross-realm admin delegation.

@pedroigor
pedroigor commented Jun 2, 2026 •
Copy link
Copy Markdown
Contributor

@ThreeMangoTrees Can you please review #49624?

It addresses the same problem, but also considers other aspects such as:

  • Not allowing managing realm management clients, including, but not restricted to, roles
  • Do not allow managing realm admin roles such as admin and create-realm roles

If you don't mind, I'll be closing this PR in favor of the above.

* see #49427
*/
@Test
@DatabaseTest
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems unnecessary to test this in database testsuite. Regular integration test should suffice,

* see #49427
*/
@Test
@DatabaseTest
@vramik vramik Jun 2, 2026 •
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, and other tests. No need to test it for DBs.

@ThreeMangoTrees
Copy link
Copy Markdown
Contributor Author

@ThreeMangoTrees Can you please review #49624?

It addresses the same problem, but also considers other aspects such as:

  • Not allowing managing realm management clients, including, but not restricted to, roles
  • Do not allow managing realm admin roles such as admin and create-realm roles

If you don't mind, I'll be closing this PR in favor of the above.

Your change is solving the problem at the root so makes more sense. Thank you for your change.

@vramik
vramik commented Jun 10, 2026
Copy link
Copy Markdown
Contributor

@ThreeMangoTrees thanks, closing now in favor of #49624

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[CVE-2026-9796] Admin role rename TOCTOU bypasses checkAdminRoles — realm-wide escalation from manage-clients

5 participants

0