Skip to content

Role-Administrators Can Modify Roles Across Organizations via Missing Target-Organization Check

Moderate
Fasse published GHSA-fcq9-w4hp-xchg Jul 20, 2026

Package

composer admidio/admidio (Composer)

Affected versions

<= 5.0.10

Patched versions

>= 5.0.11

Description

Summary

An authenticated user with the Role Administrator right (rol_assign_roles) in one organization can delete, activate, deactivate, or edit (via the save handler) a role belonging to another organization on the same Admidio install, by supplying that role's UUID to groups_roles.php. The module-level gate checks only whether the actor is a role administrator in their current organization; the delete, activate, deactivate, and save handlers never recheck that the resolved role actually belongs to that organization.

Details

Faulty code: modules/groups-roles/groups_roles.php:50-54

if ($getMode !== 'cards') {
    if (!$gCurrentUser->isAdministratorRoles()) {
        throw new Exception('SYS_NO_RIGHTS');
    }
}

isAdministratorRoles() calls checkRolesRight('rol_assign_roles'), whose query is bound to $this->organizationId (src/Users/Entity/User.php:247: WHERE ... cat_org_id = ? OR cat_org_id IS NULL) — it proves the caller administers roles in their current organization, nothing about the organization the target role belongs to.

delete, activate, and deactivate (groups_roles.php:151-186) resolve the target purely from role_uuid — a globally-unique identifier with no organization scoping — and act on it directly: $role->readDataByUuid($getRoleUUID) followed by $role->delete() / $role->activate() / $role->deactivate(). Role::delete() and the private Role::toggleValid() (src/Roles/Entity/Role.php:234, :847) only special-case default-registration, system, and administrator roles — no comparison against $gCurrentOrgId exists on this path.

save (groups_roles.php:144-149) has the same gap one level down: new RolesService($gDb, $getRoleUUID) loads the role by UUID in its constructor, and RolesService::save() (src/Roles/Service/RolesService.php:228) validates the posted CSRF/form token and writes the submitted values into that role — again with no organization recheck. Note mode=edit (rendering the edit form, GroupsRolesPresenter::createEditForm()) does check cat_org_id against $gCurrentOrgId and throws SYS_NO_RIGHTS on mismatch — the missing check is specifically on the save submission path, not on loading the form.

The missing per-record organization recheck after UUID resolution is the same general defect pattern the project has fixed elsewhere — e.g. modules/categories.php:52-67, patched for CVE-2026-47227 (a cross-module authorization bypass, not cross-organization) — but that recheck was never ported to groups_roles.php's mutating handlers.

PoC

  1. As a user who is a Role Administrator in Organization A only, obtain a role UUID belonging to Organization B (see precondition above — not obtainable from Organization A's own role list).
  2. While authenticated in the Organization A session, submit: POST /modules/groups-roles/groups_roles.php?mode=delete&role_uuid=<ORG_B_ROLE_UUID> with a valid CSRF token from that session.
  3. The response is {"status":"success"} and the role is gone from Organization B — along with its memberships, role-rights assignments, and role dependencies — despite the acting user holding no right in Organization B.
  4. The same request shape with mode=activate or mode=deactivate toggles the target organization's role instead of deleting it; mode=save (with the role's edit form fields) rewrites its properties.

DB-level verification: querying adm_roles for the target rol_uuid immediately after step 2 returns zero rows, confirming the delete executed against the real row rather than failing silently.

Impact

  • Severity: High — integrity and availability impact on a victim organization's role/permission structure; no confidentiality component (attacker destroys/alters, doesn't read).
  • Who's affected: any authenticated user holding the Role Administrator right in one organization, against any other organization on the same install, given a target role UUID.
  • Silent: the deleted/deactivated role disappears or stops granting access for the victim organization's members with no error and no cross-org-specific audit signal.
  • Blast radius: a single request against one role can strip access from every member holding that role in the victim organization.

Suggested Fix

Port the per-record organization recheck already used in GroupsRolesPresenter::createEditForm() (cat_org_id vs $gCurrentOrgId, throwing SYS_NO_RIGHTS on mismatch) into the delete, activate, deactivate handlers in groups_roles.php and into RolesService::save() before it writes the posted values.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
High
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:H/A:N

CVE ID

No known CVE

Weaknesses

Missing Authorization

The product does not perform an authorization check when an actor attempts to access a resource or perform an action. Learn more on MITRE.

Credits