Skip to content

Pre-auth(Unauthentication) Blind SQL Injection in admidio

High
Fasse published GHSA-p5cp-mhvx-w392 Aug 16, 2026

Package

composer admidio/admidio (Composer)

Affected versions

<=5.0.11

Patched versions

5.0.12

Description

Summary

The membership management feature allows access even to users who are not logged in. Additionally, the system fails to adequately validate user input for the relation_type_list parameter used when filtering the user list and lacks validation for dummy UUIDs in the role_list parameter, resulting in an SQL injection vulnerability.

Details

Vulnerability Location: modules/groups-roles/lists_show.php, src/Roles/Entity/ListConfiguration.php:989-996
Analysis confirms the absence of any prepared statement binding or security validation.

The vulnerability arises due to two instances of insufficient validation, as described below.

  • The membership management feature in modules/groups-roles/lists_show.php accepts a GET parameter named relation_type_list and uses it as a filter for user relationships.
  • While the associated role_list parameter undergoes validation using UUID::isValid() for each value, relation_type_list is not subjected to any security validation; instead, the result of explode(',', ...) is directly concatenated into an SQL IN (...) clause, with the values enclosed in single quotes (src/Roles/Entity/ListConfiguration.php:991).
  1. Missing input validation — modules/groups-roles/lists_show.php
  • role_list validates each value using UUID::isValid() and removes invalid values:
   // lists_show.php
   $getRoleList         = admFuncVariableIsValid($_GET, 'role_list', 'string');
   $getRelationTypeList = admFuncVariableIsValid($_GET, 'relation_type_list', 'string');
   ...
   $roleUuidList = explode(',', $getRoleList);
   foreach ($roleUuidList as $key => $roleUuid) {
       if (!UUID::isValid($roleUuid)) {   // role_list UUID validation
           unset($roleUuidList[$key]);
       }
   }
   ...
   // However, `relation_type_list` is converted into an array as-is, without any validation.
   if ($getRelationTypeList !== '') {
       $relationTypeUuidList = explode(',', $getRelationTypeList);   // <-- Missing Input Validation
   }
   ...
   // Unverified value sending to SQL builder
   'showRelationTypes' => $relationTypeUuidList,
  • admFuncVariableIsValid(..., 'string') applies only strip_tags() and htmlspecialchars(ENT_QUOTES). In other words, while < > " ' are encoded as HTML entities (which is irrelevant for SQL escaping), characters such as backslashes (\), commas (,), parentheses, #, =, alphanumeric characters, and spaces pass through unchanged.
  1. String concatenation — src/Roles/Entity/ListConfiguration.php:989-996
   if (count($optionsAll['showRelationTypes']) > 0) {
       $sqlUserJoin = 'INNER JOIN ' . TBL_USER_RELATION_TYPES . '
                               ON urt_uuid IN (\'' . implode('\', \'', $optionsAll['showRelationTypes']) . '\')
                       INNER JOIN ' . TBL_USER_RELATIONS . ' ...';
   }
  • This string is subsequently processed by getSQL() and executed via $gDb->queryPrepared($sql, array()) (with an empty parameter array). Since user input is inserted directly into the urt_uuid IN ('...') literal, placing a backslash at the end of the final value escapes the closing single quote added by the imploder; this breaks the string literal and causes the subsequent text to be executed as SQL.
  • Additionally, characters like # can be used to comment out the remainder of the statement. Constraints regarding the encoding of ', <, and > can be bypassed using numeric conditions, operators like = or BETWEEN, and syntax such as SUBSTRING(x FROM p FOR 1) (ANSI syntax without commas).
  • Since the result is not directly displayed on the screen, the injected subquery is crafted to return two rows, triggering a cardinality error (ER_SUBQUERY_NO_1_ROW).
  • Admidio does not suppress this error; instead, it calls ->fetchAll() at a higher level, exposing the error message Call to a member function fetchAll() on false in the response.
  • This allows for the construction of a boolean oracle, as the outcomes—true (no error) versus false (error occurs)—are clearly distinguishable.
  1. Pre-auth(Unauthentication) Vulnerability
  • lists_show.php loads only system/common.php at the top and does not enforce login (via require_once('.../system/login_valid.php')), thereby allowing unauthenticated access to the vulnerability vector.
  • The single access control currently in place is a permission check performed via a foreach loop on the roles loaded into role_list.
   foreach (... $roles as $role) {                 // Roles loaded from the database using the role_list UUID.
       if (!$gCurrentUser->hasRightViewRole($roleId) || ...) {
           throw new Exception('SYS_NO_RIGHTS');    // <-- Permission Blocking
       }
   }
  • This loop executes only when actual roles are successfully loaded using the role_list UUIDs. Consequently, if a non-existent dummy UUID (e.g., 00000000-0000-4000-8000-000000000000, which is valid in format) is provided, zero roles are loaded, and the permission-blocking logic is not triggered.
  • In other words, the permission check itself is bypassed; the subsequent code proceeds to create a ListConfiguration and execute a list query that includes showRelationTypes (the injection point). As a result, even an unauthenticated user can exploit this to launch an SQL injection attack.

PoC

  • The attack payload is as follows:
/modules/groups-roles/lists_show.php?mode=html&role_list=00000000-0000-4000-8000-000000000000&mem_show_filter=0&relation_type_list={payload}
  • Based on this, the boolean oracle can be distinguished as follows: the presence of the string "fetchAll() on false" indicates a False condition, while the presence of "No url within the navigation stack." indicates a True condition.
// True

\,) OR (1=(SELECT CASE WHEN (ASCII(SUBSTRING((SELECT VERSION()) FROM 23 FOR 1)) = 0) THEN 1 ELSE (SELECT 1 UNION SELECT 2) END))#
// False

\,) OR (1=(SELECT CASE WHEN (ASCII(SUBSTRING((SELECT VERSION()) FROM 22 FOR 1)) BETWEEN 50 AND 51) THEN 1 ELSE (SELECT 1 UNION SELECT 2) END))#
  • Consequently, this enables an attacker to enumerate Admidio's entire database schema and contents.
poc

Impact

  • An unauthenticated (not logged in) remote attacker can read the entire database with just a single GET request. No user interaction or specific privileges are required.
  • Arbitrary queries can be executed against all tables—including adm_users (containing password hashes, emails, and usernames)—as well as the information_schema.
  • As demonstrated in the PoC above, extracting and offline-cracking an administrator's bcrypt hash could lead to the hijacking of the administrator account or the mass exfiltration of sensitive data, such as stored Personally Identifiable Information (PII).
  • Compromising an administrator account can escalate (or be chained) into a full takeover of the application, enabling actions such as creating new administrators or tampering with all data.

Recommended FIX

  1. Input validation — Validate each element as a UUID, just like role_list
   // lists_show.php — relation_type_list
   if ($getRelationTypeList !== '') {
       $relationTypeUuidList = explode(',', $getRelationTypeList);
       foreach ($relationTypeUuidList as $key => $relationTypeUuid) {
           if (!UUID::isValid($relationTypeUuid)) {
               unset($relationTypeUuidList[$key]);
           }
       }
   }
  1. Parameterize the sink - Bind urt_uuid IN (...) in ListConfiguration.php using placeholders instead of string concatenation (applying the Database::getQmForValues() and binding pattern already correctly used in other queries within the same file).
   // Example
   $placeholders = Database::getQmForValues($optionsAll['showRelationTypes']); // '?, ?, ?'
   $sqlUserJoin  = 'INNER JOIN ' . TBL_USER_RELATION_TYPES . '
                            ON urt_uuid IN (' . $placeholders . ') ...';
   // And pass the `showRelationTypes` values as binding parameters to the final `queryPrepared()` call.
  • For reference, the query to retrieve relationship type names within the same lists_show.php file already uses correct binding:
    WHERE urt_uuid IN (' . Database::getQmForValues($relationTypeUuidList) . ') → queryPrepared($sql, $relationTypeUuidList). The same principle should be applied to the JOIN sink as well.
  1. Defense in Depth — If lists_show.php is not intended to allow unauthorized access, explicitly enforce login/authorization at the top (e.g., via login_valid.php or by checking module access rights) to block paths where the authorization check could be bypassed using a non-existent UUID.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

CVE ID

CVE-2026-82655

Weaknesses

Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')

The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data. Learn more on MITRE.

Credits