GHSA-G8P8-94F2-28GR

Vulnerability from github – Published: 2026-04-29 21:44 – Updated: 2026-05-08 19:55
VLAI
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:

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:

} 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:

} 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 to a role with rol_edit_user=1 but rol_administrator=0.

Step 1: Log in as the user manager account and capture the session cookie.

Step 2: Request all users across all organizations by directly calling the data endpoint:

curl -s -b 'PHPSESSID=<user_manager_session>' \
  'https://target/adm_program/modules/contacts/contacts_data.php?mem_show_filter=3&draw=1&start=0&length=100&search%5Bvalue%5D='

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.

Actual behavior: The endpoint returns a JSON response containing all users from ALL organizations in the database, including: - User UUIDs (usr_uuid) - Login names (login_name) - Email addresses (member_email) - All configured profile fields (names, addresses, phone numbers, etc.)

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.

Impact

In 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:

  • Full names and all configured profile fields
  • Email addresses
  • Login names (useful for credential attacks)
  • User UUIDs (useful for targeting other API endpoints)

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

Recommended Fix

Change line 235 in modules/contacts/contacts_data.php to match the frontend guard at contacts.php:80:

// Before (vulnerable):
} elseif (($getMembersShowFilter === 3) && $gCurrentUser->isAdministratorUsers()) {

// After (fixed):
} elseif (($getMembersShowFilter === 3) && $gCurrentUser->isAdministrator() && $gSettingsManager->getBool('contacts_show_all')) {

Additionally, 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:

if ($getMembersShowFilter === 3 && (!$gCurrentUser->isAdministrator() || !$gSettingsManager->getBool('contacts_show_all'))) {
    $getMembersShowFilter = 0; // Fall back to default
}
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 5.0.8"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "admidio/admidio"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "5.0.9"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-41657"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-29T21:44:24Z",
    "nvd_published_at": "2026-05-07T04:16:28Z",
    "severity": "MODERATE"
  },
  "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() \u0026\u0026 $gSettingsManager-\u003egetBool(\u0027contacts_show_all\u0027)) {\n    // Only then is filter=3 (\"All Organizations\") shown in the dropdown\n    $selectBoxValues = array(\n        ...\n        \u00273\u0027 =\u003e array(\u00273\u0027, $gL10n-\u003eget(\u0027SYS_ALL_CONTACTS\u0027), $gL10n-\u003eget(\u0027SYS_ALL_ORGANIZATIONS\u0027))\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) \u0026\u0026 $gCurrentUser-\u003eisAdministratorUsers()) {\n    $mainSql = $contactsListConfig-\u003egetSql(\n        array(\n            \u0027showAllMembersDatabase\u0027 =\u003e true,\n            ...\n        )\n    );\n```\nThis only requires `isAdministratorUsers()` which checks `rol_edit_user=true` \u2014 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 \u2014 full system administrator\n- `isAdministratorUsers()` (line 1625): checks `rol_edit_user` right \u2014 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[\u0027showAllMembersDatabase\u0027]) {\n    $sql = \u0027SELECT DISTINCT \u0027 . $sqlMemLeader . $sqlIdColumns . $sqlColumnNames . \u0027\n              FROM \u0027 . TBL_USERS . \u0027\n                   \u0027 . $sqlJoin . \u0027\n             WHERE usr_valid = true \u0027 .\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 \u2014 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 \u0027PHPSESSID=\u003cuser_manager_session\u003e\u0027 \\\n  \u0027https://target/adm_program/modules/contacts/contacts_data.php?mem_show_filter=3\u0026draw=1\u0026start=0\u0026length=100\u0026search%5Bvalue%5D=\u0027\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 \u2014 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) \u0026\u0026 $gCurrentUser-\u003eisAdministratorUsers()) {\n\n// After (fixed):\n} elseif (($getMembersShowFilter === 3) \u0026\u0026 $gCurrentUser-\u003eisAdministrator() \u0026\u0026 $gSettingsManager-\u003egetBool(\u0027contacts_show_all\u0027)) {\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 \u0026\u0026 (!$gCurrentUser-\u003eisAdministrator() || !$gSettingsManager-\u003egetBool(\u0027contacts_show_all\u0027))) {\n    $getMembersShowFilter = 0; // Fall back to default\n}\n```",
  "id": "GHSA-g8p8-94f2-28gr",
  "modified": "2026-05-08T19:55:36Z",
  "published": "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Admidio Exposes Cross-Organization Member Data via Permission Check Mismatch in contacts_data.php"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…