| タイトル | Formalms forma.lms 4.1.43 SQL Injection |
|---|
| 説明 | forma.lms 4.1.43 contains an SQL injection vulnerability in the multi-user-selector AJAX endpoint. Any authenticated user, including an ordinary student account with no administrative privileges, can inject arbitrary SQL into several database queries by controlling the name of a DataTables filter column. This allows reading the entire contents of the application database, including the password hashes of every user.
AFFECTED ENDPOINT
HTTP POST to: /appCore/ajax.adm_server.php?r=adm/userselector/getData
Handled by UserselectorAdmController::getDataTask(), file
html/appCore/controllers/UserselectorAdmController.php line 174.
ROOT CAUSE
The endpoint accepts parameters in DataTables server-side processing format. The value of columns[i][name] is taken directly from the request and used as the ARRAY KEY of the column filter map, so it undergoes no filtering or validation. This happens in three data
selectors:
html/lib/Selectors/Multiuserselector/DataSelectors/UserDataSelector.php line 36
html/lib/Selectors/Multiuserselector/DataSelectors/GroupDataSelector.php line 29
html/lib/Selectors/Multiuserselector/DataSelectors/RoleDataSelector.php line 30
The code is:
foreach ($columns as $column) {
if ($column['search']['value'] != '') {
$columnsFilter[$column['name']] = $column['search']['value'];
}
}
That key is later concatenated into SQL as an UNQUOTED IDENTIFIER, in 13 distinct locations:
html/appCore/models/UsermanagementAdm.php lines 405, 441, 481, 517, 553, 848, 983
html/appCore/models/GroupmanagementAdm.php lines 106, 225, 247
html/appCore/models/FunctionalrolesAdm.php lines 149, 230, 277
The vulnerable pattern is:
$query .= ' AND (
u.' . $columnName . ' LIKE "%' . $columnValue . '%"
)';
The application applies addslashes() plus HTMLPurifier to all request input during the boot step BOOT_INPUT (html/lib/lib.filterinput.php). This is ineffective here for two independent reasons.
First, the injected value is an SQL IDENTIFIER, not a string literal: it is not enclosed in
quotes, so an attacker does not need quote characters to alter the query, and escaping
quotes provides no protection. Note that the filter VALUE is enclosed in double quotes and
is therefore protected; only the column NAME is exploitable.
Second, the controller does not read the sanitized superglobals. It reads from the Symfony
HttpFoundation Request object, which is created by Request::createFromGlobals() during boot
step BOOT_REQUEST (constant value 5, html/base.php line 95), whereas sanitization happens in
BOOT_INPUT (constant value 11, html/base.php line 101). Because the superglobals are
snapshotted before sanitization runs, the values read at
html/appCore/controllers/UserselectorAdmController.php line 44 are completely unsanitized.
PRIVILEGE REQUIRED
Any authenticated session. UserselectorAdmController overrides the base class initializer without calling parent::init() and performs no permission check of its own, so the administrative permission gate is never executed. This missing authorization is reported
separately. The only control on the request path is the anti-CSRF session token (parameter authentic_request, or header X-Signature), which every authenticated user legitimately possesses and can read from the source of their own pages.
This was confirmed by testing: a student account with no administrative privileges receives HTTP 200 and a valid JSON response from this endpoint.
The exploitable branch requires the global search parameter to be empty and the per-column search value to be non-empty; both conditions are trivially met.
PROOF OF CONCEPT
Validated on a live forma.lms 4.1.43 instance running MariaDB 10.11 and PHP 8.4,
authenticated as a student account with no administrative privileges.
Request:
POST /appCore/ajax.adm_server.php?r=adm/userselector/getData
Body (URL-encoded):
dataType=group
op=
search[value]=
columns[0][name]=groupid) OR (SELECT SLEEP(3)) OR (g.groupid
columns[0][search][value]=x
authentic_request=<session anti-CSRF token>
Measured response times:
Baseline, columns[0][name]=groupid 0.013 seconds
Injection, payload above 36.0 seconds
The query actually executed, captured from the MySQL general query log, was:
SELECT COUNT(*) FROM core_group as g
WHERE g.hidden = 'false' AND g.type <> 'course'
AND ( g.groupid) OR (SELECT SLEEP(3)) OR (g.groupid LIKE "%x%" )
The payload closes the parenthesis opened by the query template, injects an uncorrelated scalar subquery that MySQL evaluates once, and re-opens a parenthesis so that the trailing LIKE clause and closing parenthesis of the template remain syntactically valid.
The same vector is reachable against three datasets by varying the dataType parameter: user, group and role, each mapping to its own set of sinks listed above.
IMPACT
An authenticated user holding the lowest privilege level on the platform can extract the full contents of the database via blind SQL injection, including all rows of core_user and therefore every user's password hash.
|
|---|
| ソース | ⚠️ https://github.com/lorenzobruno7/advisory_formalms_sql/tree/main |
|---|
| ユーザー | Brunlorenz (UID 95709) |
|---|
| 送信 | 2026年07月25日 14:18 (2 月 ago) |
|---|
| モデレーション | 2026年09月23日 18:22 (2 months later) |
|---|
| ステータス | 承諾済み |
|---|
| VulDBエントリ | 409034 [Forma LMS 迄 4.1.43 Multi-User-Selector AJAX Endpoint getData getDataTask 名前 SQLインジェクション] |
|---|
| ポイント | 20 |
|---|