{"id":"GHSA-ph84-r98x-2j22","summary":"Admidio has Missing CSRF Protection on Registration Approval Actions","details":"## Summary\n\nThe create_user, assign_member, and assign_user action modes in modules/registration.php approve pending user registrations via GET request without validating a CSRF token. Unlike the delete_user mode in the same file (which correctly validates the token), these three approval actions read their parameters from $_GET and perform irreversible state changes without any protection. An attacker who has submitted a pending registration can extract their own user UUID from the registration confirmation email URL, then trick any user with the rol_approve_users right into visiting a crafted URL that automatically approves the registration. This bypasses the manual registration approval workflow entirely.\n\n## Details\n\n### CSRF Protection Is Present for delete_user but Absent for Approval Modes\n\nFile: modules/registration.php, lines 90-128\n\nThe delete_user mode validates the CSRF token (line 99), but the three approval modes do not:\n\n```php\n// assign_member and assign_user: no CSRF check\n} elseif (in_array($getMode, array('assign_member', 'assign_user'))) {\n    $registrationService = new RegistrationService($gDb, $getUserUUID);\n    $message = $registrationService-\u003eassignRegistration($getUserUUIDAssigned, $getMode === 'assign_member');\n    $gMessage-\u003esetForwardUrl($message['forwardUrl']);\n    $gMessage-\u003eshow($message['message']);\n\n// create_user: no CSRF check\n} elseif ($getMode === 'create_user') {\n    $registrationUser-\u003eacceptRegistration();\n    if ($gCurrentUser-\u003eisAdministratorRoles()) {\n        admRedirect(SecurityUtils::encodeUrl(ADMIDIO_URL . FOLDER_MODULES.'/profile/roles.php',\n            array('accept_registration' =\u003e true, 'user_uuid' =\u003e $getUserUUID)));\n    }\n\n// delete_user: CSRF IS validated\n} elseif ($getMode === 'delete_user') {\n    SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']); // \u003c-- protected\n    $registrationUser-\u003edelete();\n}\n```\n\nThe three approval modes read both UUIDs exclusively from $_GET (lines 41-43):\n\nThe approve action modes accept $_GET parameters `user_uuid` and `user_uuid_assigned` without any POST body or CSRF token. Both parameters pass through `admFuncVariableIsValid()` with `uuid` type validation, which prevents SQL injection but provides no CSRF protection.\n\n### User UUID Is Known to the Attacker from Registration Email\n\nFile: `D:/bugcrowd/admidio/repo/src/Infrastructure/Service/RegistrationService.php`, lines 154-157\n\nWhen a user submits a registration, Admidio sends a confirmation email containing a URL of the form:\n\n```\nhttps://TARGET/adm_program/modules/registration.php?id=VALIDATION_ID&user_uuid=REGISTRANT_UUID\n```\n\nThe `user_uuid` in this URL is the registrant's own UUID. The attacker has this UUID because they received the confirmation email for their own registration.\n\n### isAdministratorRegistration() Is a Delegated Right\n\nFile: `D:/bugcrowd/admidio/repo/src/Users/Entity/User.php`, lines 1603-1606\n\n```php\npublic function isAdministratorRegistration(): bool\n{\n    return $this-\u003echeckRolesRight('rol_approve_users');\n}\n```\n\nThe `rol_approve_users` right is a delegated organizational privilege, not full system administrator access. Any member designated to review registrations -- for example, a membership secretary or club administrator -- is a valid CSRF victim.\n\n## PoC\n\n**Scenario: Attacker bypasses manual registration approval**\n\nPrerequisites: (1) Manual registration approval is enabled. (2) The attacker submits a registration form and receives a confirmation email with their `user_uuid`. (3) After clicking the confirmation link, their registration enters the pending queue.\n\n**Step 1: Attacker extracts their own user_uuid from the registration email**\n\nThe confirmation email contains a link of the form:\n\n```\nhttps://TARGET/adm_program/modules/registration.php?id=VALIDATION_ID&user_uuid=ATTACKER_UUID\n```\n\nThe `ATTACKER_UUID` is visible to the attacker from their own email.\n\n**Step 2: CSRF auto-approval via image tag**\n\nThe attacker hosts a page that the victim (admin with `rol_approve_users` right) visits:\n\n```html\n\u003cimg src=\"https://TARGET/adm_program/modules/registration.php?mode=create_user&user_uuid=ATTACKER_UUID\" width=\"1\" height=\"1\"\u003e\n```\n\nWhen the victim loads this page, Admidio silently accepts the attacker registration and assigns default organization roles. No confirmation or token is required.\n\n**Step 3: Force-assign registration to an existing account (account takeover)**\n\nIf the attacker knows the UUID of an existing member (obtainable from profile page URLs when the user list is visible) and has a pending registration:\n\n```html\n\u003cimg src=\"https://TARGET/adm_program/modules/registration.php?mode=assign_user&user_uuid=ATTACKER_REG_UUID&user_uuid_assigned=EXISTING_USER_UUID\" width=\"1\" height=\"1\"\u003e\n```\n\nThis merges the pending registration into the existing account, replacing that account login credentials with the attacker credentials.\n\n## Impact\n\n- **Manual Approval Bypass:** An attacker with a pending registration can force auto-approval without waiting for an administrator to manually review it. This grants them organization membership, including access to events, documents, mailing lists, and other role-restricted features.\n- **Account Takeover via assign_user CSRF:** If the attacker knows any member UUID (visible in profile page URLs), the `assign_user` mode merges the attacker registration into that member account, replacing the existing member login with the attacker credentials. This is a full account takeover requiring only that the victim admin visit a crafted URL.\n- **Low Attack Complexity:** The attacker only needs their own registration email to get their UUID. The CSRF payload is a plain GET request via an image tag -- no JavaScript required.\n- **Delegated Right:** The required victim right (`rol_approve_users`) is a common delegation target in organizations with membership approval workflows.\n\n## Recommended Fix\n\nAdd `SecurityUtils::validateCsrfToken($_POST[\"adm_csrf_token\"])` at the beginning of each approval action, consistent with how `delete_user` is already protected in the same file.\n\n```php\n// File: modules/registration.php\n\n} elseif (in_array($getMode, array('assign_member', 'assign_user'))) {\n    // ADD: validate CSRF token\n    SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']);\n    $registrationService = new RegistrationService($gDb, $getUserUUID);\n    $message = $registrationService-\u003eassignRegistration($getUserUUIDAssigned, $getMode === 'assign_member');\n    ...\n\n} elseif ($getMode === 'create_user') {\n    // ADD: validate CSRF token\n    SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']);\n    $registrationUser-\u003eacceptRegistration();\n    ...\n\n} elseif ($getMode === 'delete_user') {\n    SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']); // already protected\n    $registrationUser-\u003edelete();\n}\n```\n\nAdditionally, convert the approval action URLs from GET-based links to POST-form buttons (with the CSRF token in a hidden field). The existing `delete_user` button uses `callUrlHideElement()` which already sends the token in the POST body -- use the same pattern for approval buttons.","aliases":["CVE-2026-34384"],"modified":"2026-03-31T23:26:26.734874Z","published":"2026-03-31T23:11:24Z","database_specific":{"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-03-31T23:11:24Z","nvd_published_at":"2026-03-31T21:16:30Z","cwe_ids":["CWE-352"]},"references":[{"type":"WEB","url":"https://github.com/Admidio/admidio/security/advisories/GHSA-ph84-r98x-2j22"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-34384"},{"type":"WEB","url":"https://github.com/Admidio/admidio/commit/707171c188b3e8f36007fc3f2bccbfac896ed019"},{"type":"PACKAGE","url":"https://github.com/Admidio/admidio"}],"affected":[{"package":{"name":"admidio/admidio","ecosystem":"Packagist","purl":"pkg:composer/admidio/admidio"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"5.0.8"}]}],"versions":["4.1.0","4.1.3","v4.2-Beta.1","v4.2-Beta.2","v4.2-Beta.3","v4.2.0","v4.2.1","v4.2.10","v4.2.11","v4.2.12","v4.2.13","v4.2.14","v4.2.2","v4.2.3","v4.2.4","v4.2.5","v4.2.6","v4.2.7","v4.2.8","v4.2.9","v4.3-Beta.1","v4.3-Beta.3","v4.3-Beta.4","v4.3-Beta.5","v4.3.0","v4.3.1","v4.3.10","v4.3.11","v4.3.12","v4.3.13","v4.3.14","v4.3.15","v4.3.16","v4.3.17","v4.3.2","v4.3.3","v4.3.4","v4.3.5","v4.3.6","v4.3.7","v4.3.8","v4.3.9","v5.0-Beta.1","v5.0-Beta.2","v5.0-Beta.3","v5.0.0","v5.0.1","v5.0.2","v5.0.3","v5.0.4","v5.0.5","v5.0.6","v5.0.7"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-ph84-r98x-2j22/GHSA-ph84-r98x-2j22.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:U/C:N/I:H/A:N"}]}