sec: enforce admin check on layout assignment and role grant endpoints#36344
sec: enforce admin check on layout assignment and role grant endpoints#36344mbiuki wants to merge 3 commits into
Conversation
ToolGroupResource._addtouser and ._removefromuser now require the caller to be a CMS Administrator before assigning or removing any layout other than the gettingStarted onboarding layout. Previously any backend user could self-assign the admin Settings layout (which contains the roles portlet), bypassing the intended privilege boundary. RoleAjax.addUserToRole and .removeUsersFromRole now require the caller to be a CMS Administrator in addition to the existing portlet-access check. Previously a user who had gained roles-portlet access could grant themselves (or any user) any role including CMS Administrator via DWR. Together these two gaps formed a privilege-escalation chain that allowed any authenticated backend user to reach CMS Administrator and subsequently execute arbitrary OS commands via OSGi bundle upload. Closes: dotCMS/private-issues#640 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
❌ Issue Linking RequiredThis PR could not be linked to an issue. All PRs must be linked to an issue for tracking purposes. How to fix this:Option 1: Add keyword to PR body (Recommended - auto-removes this comment)
Why is this required?Issue linking ensures proper tracking, documentation, and helps maintain project history. It connects your code changes to the problem they solve.--- This comment was automatically generated by the issue linking workflow |
🤖 dotBot Review (Bedrock)Reviewed 2 file(s); 1 candidate(s) → 1 confirmed, 0 uncertain (unverified, kept for review). Confirmed findings
us.deepseek.r1-v1:0 · Run: #28671877655 · tokens: in: 8448 · out: 2919 · total: 11367 · calls: 4 · est. ~$0.027 |
|
This PR fixes https://github.com/dotCMS/private-issues/issues/642 |
PRs linked to this issue |
Versions AffectedExploitable range: v21.02 → v26.06.22-03 (current latest)
No patched release has been cut yet — this PR is the fix. |
Vulnerability Introduction Timeline
|
| Date | Commit | Author | Event |
|---|---|---|---|
| 2020-12-16 | 8aa87f7b87 |
Will Ezell | Method created without any admin check (PR #19680, issue #19581) — only requiredBackendUser(true) enforced |
| 2021-03-22 | 890feffa94 |
Nollymar Longa | ?userid= parameter added — widened exploit surface from self-assignment to assigning layouts to any user |
| 2025-11-04 | 3babaf0d17 |
— | Partial portlet-level mitigation added (private-issues#482) — insufficient alone |
| PR #36344 | — | — | Full fix: admin-only gate added — not yet in any release |
Affected Versions
- First vulnerable release:
21.02(December 2020 commit shipped in the Feb 2021 release cut) - Widened attack surface (other-user assignment): ~
21.03–21.04(March 2021 commit) - All affected:
21.02→v26.06.22-03(current latest), including all LTS lines:- 23.10 LTS ✗
- 24.04 LTS ✗
- 24.12 LTS ✗
- 25.07 LTS ✗
- Partial mitigation from:
v25.11.07-1(still exploitable via chain with sec: enforce admin check on layout assignment and role grant endpoints #36344) - Fully fixed in: not yet released
🔴 Red Team Review — PR #36344Three independent reviewers examined this PR across security, performance, and test coverage. Verdicts
🔐 SecurityThe 🚨 Critical —
|
| Scenario | Status |
|---|---|
Non-admin blocked on _addtouser |
✅ |
Admin succeeds on _addtouser |
✅ |
Non-admin blocked on addUserToRole (with portlet access) |
✅ |
gettingStarted non-admin self-assignment allowed |
❌ Missing |
Non-admin blocked on _removefromuser |
❌ Missing |
Admin succeeds on _removefromuser |
❌ Missing |
Non-admin blocked on removeUsersFromRole |
❌ Missing |
Admin succeeds on addUserToRole / removeUsersFromRole |
❌ Missing |
Cross-user ?userid= assignment blocked for non-admin |
❌ Missing |
SecurityLogger invoked on unauthorized attempts |
❌ Missing |
Recommended additions:
// ToolGroupResourceTest
test_removeToolGroupFromUser_lowPrivilegeUser_throwsSecurityException()
test_removeToolGroupFromUser_adminUser_succeeds()
test_addToolGroupToUser_gettingStartedLayout_nonAdmin_succeeds() // if exception is restored
test_addToolGroupToUser_crossUserAssignment_lowPrivilegeUser_throwsSecurityException()
// RoleAjaxSecurityTest
test_removeUsersFromRole_lowPrivilegeUser_throwsSecurityException()
test_addUserToRole_adminUser_succeeds()
test_removeUsersFromRole_adminUser_succeeds()Summary
The _addtouser fix is correct and closes Step 1 of the escalation chain. Three blockers must be resolved before merge:
- 🚨 Add admin guard to
_removefromuser ⚠️ ReplacegetAdminUser()withisAdmin()inaddUserToRole+removeUsersFromRole⚠️ RestoreSecurityLoggerwith accurate action descriptions on unauthorized role-assignment attempts
Move the isAdmin() check above the loadUserById() call in both _removefromuser and _addtouser so unauthorized callers are rejected before any DB round-trip for the target user. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JHg1W7beD4Z1yLoTJpyXss
|
Correction to the Red Team Review above After reading the actual PR commit (
The review was based on pre-PR baseline code rather than the PR branch — apologies for the noise. One real issue was found and fixed: both |
Summary
Fixes a privilege-escalation chain (reported via responsible disclosure) that allowed any authenticated backend user to reach CMS Administrator and subsequently achieve remote code execution via OSGi bundle upload.
Attack chain closed by this PR:
PUT /api/v1/toolgroups/{id}/_addtouser— any backend user could self-assign the admin Settings layout (containing therolesportlet) with no privilege checkPOST /dwr/call/plaincall/RoleAjax.addUserToRole.dwr— with therolesportlet now accessible, the user could grant themselves the CMS Administrator role; the only gate was portlet-access, which step 1 satisfiedChanges
ToolGroupResource.java_addtouser: requireloggedInUser.isAdmin()for all layouts exceptgettingstarted(the onboarding layout — intentionally self-assignable)_removefromuser: same admin guard applied symmetricallyRoleAjax.javaaddUserToRole: addcaller.isAdmin()check after the existing portlet-access check; log unauthorized attempts viaSecurityLoggerremoveUsersFromRole: same admin guard applied symmetricallyTest plan
PUT /api/v1/toolgroups/{layout_id}/_addtouser(non-gettingStarted layout)gettingStartedlayout self-assignment still works for non-admin users (onboarding flow must not break)RoleAjax.addUserToRolevia DWRReferences
🤖 Generated with Claude Code