{"id":"CVE-2026-41657","aliases":["GHSA-g8p8-94f2-28gr"],"url":"https://o3.security/vulnerability/CVE-2026-41657","summary":"Admidio: Cross-Organization Member Data Exposure via Permission Check Mismatch in contacts_data.php","details":"## Summary\n\nThe `contacts_data.php` endpoint uses a weaker permission check (`isAdministratorUsers()`, requiring only `rol_edit_user=true`) than the frontend UI (`contacts.php`) which correctly requires the stronger `isAdministrator()` (requiring `rol_administrator=true`) and the `contacts_show_all` system setting. A user manager who is not a full administrator can directly request `contacts_data.php?mem_show_filter=3` to retrieve all user records across all organizations in the Admidio instance, bypassing multi-tenant organization isolation.\n\n## Details\n\nThe frontend page `contacts.php` and the backend data endpoint `contacts_data.php` have mismatched authorization checks for the \"show all organizations\" filter (`mem_show_filter=3`).\n\n**Frontend guard** at `modules/contacts/contacts.php:80`:\n```php\nif ($gCurrentUser->isAdministrator() && $gSettingsManager->getBool('contacts_show_all')) {\n    // Only then is filter=3 (\"All Organizations\") shown in the dropdown\n    $selectBoxValues = array(\n        ...\n        '3' => array('3', $gL10n->get('SYS_ALL_CONTACTS'), $gL10n->get('SYS_ALL_ORGANIZATIONS'))\n    );\n}\n```\nThis correctly requires both `isAdministrator()` (`rol_administrator=true`) AND the `contacts_show_all` setting.\n\n**Backend check** at `modules/contacts/contacts_data.php:235`:\n```php\n} elseif (($getMembersShowFilter === 3) && $gCurrentUser->isAdministratorUsers()) {\n    $mainSql = $contactsListConfig->getSql(\n        array(\n            'showAllMembersDatabase' => true,\n            ...\n        )\n    );\n```\nThis only requires `isAdministratorUsers()` which checks `rol_edit_user=true` — a weaker permission available to non-admin \"user manager\" roles. The `contacts_show_all` setting is never checked.\n\n**The critical difference between the two methods** (from `src/Users/Entity/User.php`):\n- `isAdministrator()` (line 1507): checks the `rol_administrator` flag — full system administrator\n- `isAdministratorUsers()` (line 1625): checks `rol_edit_user` right — user management module access only\n\nWhen `showAllMembersDatabase=true` reaches `ListConfiguration::getSql()` (at `src/Roles/Entity/ListConfiguration.php:1022-1028`), the generated SQL removes ALL organization filtering:\n```php\n} elseif ($optionsAll['showAllMembersDatabase']) {\n    $sql = 'SELECT DISTINCT ' . $sqlMemLeader . $sqlIdColumns . $sqlColumnNames . '\n              FROM ' . TBL_USERS . '\n                   ' . $sqlJoin . '\n             WHERE usr_valid = true ' .\n        $sqlWhere .\n        $sqlOrderBys;\n}\n```\n\nCompare with the default query which includes `cat_org_id = $gCurrentOrgId` to restrict results to the current organization.\n\nThe cross-org indicator subqueries at line 169 do correctly check `isAdministrator()`, so the `member_other_orga` columns return 0 — but this only affects display indicators, not the actual user data returned.\n\n## PoC\n\n**Prerequisites:** An Admidio instance with at least two organizations sharing the same database. A user account in Organization A assigned to a role with `rol_edit_user=1` but `rol_administrator=0`.\n\n**Step 1:** Log in as the user manager account and capture the session cookie.\n\n**Step 2:** Request all users across all organizations by directly calling the data endpoint:\n```bash\ncurl -s -b 'PHPSESSID=<user_manager_session>' \\\n  'https://target/adm_program/modules/contacts/contacts_data.php?mem_show_filter=3&draw=1&start=0&length=100&search%5Bvalue%5D='\n```\n\n**Expected behavior:** The request should be rejected or return only current-organization users, since the user is not a full administrator and the frontend never offers filter=3 to non-administrators.\n\n**Actual behavior:** The endpoint returns a JSON response containing all users from ALL organizations in the database, including:\n- User UUIDs (`usr_uuid`)\n- Login names (`login_name`)\n- Email addresses (`member_email`)\n- All configured profile fields (names, addresses, phone numbers, etc.)\n\n**Step 3:** Verify that users from Organization B (where the attacker has no membership) appear in the results by checking the `member_this_orga` field — it will be `0` for cross-org users.\n\n## Impact\n\nIn multi-organization Admidio deployments (the primary use case for organization isolation), a user manager in one organization can exfiltrate the complete member directory of all other organizations sharing the same database. Exposed data includes:\n\n- Full names and all configured profile fields\n- Email addresses\n- Login names (useful for credential attacks)\n- User UUIDs (useful for targeting other API endpoints)\n\nThis completely bypasses the multi-tenant organization isolation boundary. The `contacts_show_all` admin setting (intended to control this feature) is also bypassed, meaning even instances where administrators have explicitly disabled cross-org viewing are affected.\n\n## Recommended Fix\n\nChange line 235 in `modules/contacts/contacts_data.php` to match the frontend guard at `contacts.php:80`:\n\n```php\n// Before (vulnerable):\n} elseif (($getMembersShowFilter === 3) && $gCurrentUser->isAdministratorUsers()) {\n\n// After (fixed):\n} elseif (($getMembersShowFilter === 3) && $gCurrentUser->isAdministrator() && $gSettingsManager->getBool('contacts_show_all')) {\n```\n\nAdditionally, as defense-in-depth, add an early rejection at the top of the file (after line 59) to block the filter value entirely for unauthorized users:\n\n```php\nif ($getMembersShowFilter === 3 && (!$gCurrentUser->isAdministrator() || !$gSettingsManager->getBool('contacts_show_all'))) {\n    $getMembersShowFilter = 0; // Fall back to default\n}\n```","published":"2026-05-07T02:58:09.340Z","modified":"2026-08-12T03:51:09.067754434Z","cvss":{"score":4.9,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N"},"epss":{"score":0.00322,"percentile":0.24884,"asOf":"2026-08-13"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"admidio/admidio","fixedVersion":"5.0.9"}],"fix":null,"references":[{"type":"WEB","url":"https://github.com/Admidio/admidio/releases/tag/v5.0.9"},{"type":"ADVISORY","url":"https://github.com/Admidio/admidio/security/advisories/GHSA-g8p8-94f2-28gr"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/41xxx/CVE-2026-41657.json"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-41657"},{"type":"PACKAGE","url":"https://github.com/Admidio/admidio"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:09.067754434Z"}}