GitHub Security Advisory: GHSA-g8p8-94f2-28gr
Package: admidio/admidio (COMPOSER)
Severity: MODERATE (CVSS 4.9)
CWE: CWE-863: Incorrect Authorization
Vulnerable versions: <= 5.0.8
Patched in: 5.0.9

Summary: Admidio Exposes Cross-Organization Member Data via Permission Check Mismatch in contacts_data.php

Details:
## Summary

The `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.

## Details

The 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`).

**Frontend guard** at `modules/contacts/contacts.php:80`:
```php
if ($gCurrentUser->isAdministrator() && $gSettingsManager->getBool('contacts_show_all')) {
    // Only then is filter=3 ("All Organizations") shown in the dropdown
    $selectBoxValues = array(
        ...
        '3' => array('3', $gL10n->get('SYS_ALL_CONTACTS'), $gL10n->get('SYS_ALL_ORGANIZATIONS'))
    );
}
```
This correctly requires both `isAdministrator()` (`rol_administrator=true`) AND the `contacts_show_all` setting.

**Backend check** at `modules/contacts/contacts_data.php:235`:
```php
} elseif (($getMembersShowFilter === 3) && $gCurrentUser->isAdministratorUsers()) {
    $mainSql = $contactsListConfig->getSql(
        array(
            'showAllMembersDatabase' => true,
            ...
        )
    );
```
This 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.

**The critical difference between the two methods** (from `src/Users/Entity/User.php`):
- `isAdministrator()` (line 1507): checks the `rol_administrator` flag — full system administrator
- `isAdministratorUsers()` (line 1625): checks `rol_edit_user` right — user management module access only

When `showAllMembersDatabase=true` reaches `ListConfiguration::getSql()` (at `src/Roles/Entity/ListConfiguration.php:1022-1028`), the generated SQL removes ALL organization filtering:
```php
} elseif ($optionsAll['showAllMembersDatabase']) {
    $sql = 'SELECT DISTINCT ' . $sqlMemLeader . $sqlIdColumns . $sqlColumnNames . '
              FROM ' . TBL_USERS . '
                   ' . $sqlJoin . '
             WHERE usr_valid = true ' .
        $sqlWhere .
        $sqlOrderBys;
}
```

Compare with the default query which includes `cat_org_id = $gCurrentOrgId` to restrict results to the current organization.

The 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.

## PoC

**Prerequisites:** An Admidio instance with at least two organizations sharing the same database. A user account in Organization A assigned