{"id":"GHSA-g8p8-94f2-28gr","summary":"Admidio Exposes Cross-Organization Member Data 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-\u003eisAdministrator() && $gSettingsManager-\u003egetBool('contacts_show_all')) {\n    // Only then is filter=3 (\"All Organizations\") shown in the dropdown\n    $selectBoxValues = array(\n        ...\n        '3' =\u003e array('3', $gL10n-\u003eget('SYS_ALL_CONTACTS'), $gL10n-\u003eget('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-\u003eisAdministratorUsers()) {\n    $mainSql = $contactsListConfig-\u003egetSql(\n        array(\n            'showAllMembersDatabase' =\u003e 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=\u003cuser_manager_session\u003e' \\\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-\u003eisAdministratorUsers()) {\n\n// After (fixed):\n} elseif (($getMembersShowFilter === 3) && $gCurrentUser-\u003eisAdministrator() && $gSettingsManager-\u003egetBool('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-\u003eisAdministrator() || !$gSettingsManager-\u003egetBool('contacts_show_all'))) {\n    $getMembersShowFilter = 0; // Fall back to default\n}\n```","aliases":["CVE-2026-41657"],"modified":"2026-05-08T20:07:46.383930Z","published":"2026-04-29T21:44:24Z","database_specific":{"nvd_published_at":"2026-05-07T04:16:28Z","cwe_ids":["CWE-863"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-04-29T21:44:24Z"},"references":[{"type":"WEB","url":"https://github.com/Admidio/admidio/security/advisories/GHSA-g8p8-94f2-28gr"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-41657"},{"type":"PACKAGE","url":"https://github.com/Admidio/admidio"},{"type":"WEB","url":"https://github.com/Admidio/admidio/releases/tag/v5.0.9"}],"affected":[{"package":{"name":"admidio/admidio","ecosystem":"Packagist","purl":"pkg:composer/admidio/admidio"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"5.0.9"}]}],"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","v5.0.8"],"database_specific":{"last_known_affected_version_range":"\u003c= 5.0.8","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-g8p8-94f2-28gr/GHSA-g8p8-94f2-28gr.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N"}]}