CVE-2023-4928: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') (CWE-89)
value:  SQL Injection in GitHub repository instantsoft/icms2 prior to 2.16.1.
CWE description: 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.
CVSS v3 base score: 7.2
Published: 2023-09-13T01:15Z
Repository: https://github.com/instantsoft/icms2
Language: PHP
Files changed: system/config/version.ini, system/controllers/admin/frontend.php, system/controllers/moderation/backend/actions/logs.php, system/core/grid.php, system/core/model.php, system/traits/controllers/actions/listgrid.php, update/current/manifest.en.ini, update/current/manifest.ru.ini
Commit date: 2023-09-12 18:12:27 +0300
Commit message: Fix SQL Injection in datagrid advanced filter
Diff: diff --git a/system/config/version.ini b/system/config/version.ini
index d803e1dda..da7833353 100755
--- a/system/config/version.ini
+++ b/system/config/version.ini
@@ -1,5 +1,5 @@
 [version]
 major = 2
 minor = 16
 build = 1
-date = 20230613
\ No newline at end of file
+date = 20230912
\ No newline at end of file
diff --git a/system/controllers/admin/frontend.php b/system/controllers/admin/frontend.php
index c55e7e3f5..7b51e6153 100755
--- a/system/controllers/admin/frontend.php
+++ b/system/controllers/admin/frontend.php
@@ -705,15 +705,23 @@ class admin extends cmsFrontend {
 
         $index = 0;
 
         foreach ($controllers as $controller_name) {
 
-            $ctrl_file = $this->cms_config->root_path . 'system/controllers/'.$controller_name.'/frontend.php';
-            if(!is_readable($ctrl_file)){ continue; }
+            if (!cmsController::enabled($controller_name)) {
+                continue;
+            }
+
+            $ctrl_file = $this->cms_config->root_path . 'system/controllers/' . $controller_name . '/frontend.php';
+            if (!is_readable($ctrl_file)) {
+                continue;
+            }
 
             $hooks = cmsCore::getFilesList('system/controllers/' . $controller_name . '/hooks', '*.php', true, true);
-            if (!$hooks) { continue; }
+            if (!$hooks) {
+                continue;
+            }
 
             $controller_object = cmsCore::getController($controller_name);
 
             foreach ($hooks as $event_name) {
 
@@ -722,11 +730,11 @@ class admin extends cmsFrontend {
                 $hook_object = new $hook_class_name($controller_object);
 
                 // Некоторые хуки не требуют регистрации в базе данных,
                 // Например, хуки для CRON или иные, которые вызываются напрямую
                 // Свойство $disallow_event_db_register в классе хука регулирует это поведение
-                if(empty($hook_object->disallow_event_db_register)){
+                if (empty($hook_object->disallow_event_db_register)) {
 
                     $events[$controller_name][$index] = $event_name;
 
                     $index++;
                 }
diff --git a/system/controllers/moderation/backend/actions/logs.php b/system/controllers/moderation/backend/actions/logs.php
index b5fedb339..42d541cbd 100755
--- a/system/controllers/moderation/backend/actions/logs.php
+++ b/system/controllers/moderation/backend/actions/logs.php
@@ -1,102 +1,98 @@
 <?php
 
 class actionModerationLogs extends cmsAction {
 
-    public function run($target_controller = null, $target_subject = null, $target_id = null, $moderator_id = null){
+    public function run($target_controller = null, $target_subject = null, $target_id = null, $moderator_id = null) {
 
         cmsCore::loadAllControllersLanguages();
 
         $grid = $this->loadDataGrid('logs');
 
-        $url           = href_to($this->root_url, 'logs');
-        $sub_url       = array();
-        $url_query     = array();
-        $additional_h1 = array();
+        $url             = href_to($this->root_url, 'logs');
+        $sub_url         = [];
+        $url_query       = [];
+        $additional_h1   = [];
         $subj_controller = null;
 
-        $action = $this->request->get('action', -1);
+        $action         = $this->request->get('action', -1);
         $only_to_delete = $this->request->get('only_to_delete', 0);
-        if($action > -1){
+
+        if ($action > -1) {
 
             $this->model->filterEqual('action', $action);
 
-            if($only_to_delete){
+            if ($only_to_delete) {
 
                 $this->model->filterNotNull('date_expired');
 
                 $url_query['only_to_delete'] = $only_to_delete;
-
             }
 
-            $additional_h1[] = string_lang('LANG_MODERATION_ACTION_'.$action);
+            $additional_h1[] = string_lang('LANG_MODERATION_ACTION_' . $action);
 
             $url_query['action'] = $action;
-
         }
 
-        if(!empty($target_controller)){
+        if (!empty($target_controller)) {
 
             $subj_controller = cmsCore::getController($target_controller);
 
             $this->model->filterEqual('target_controller', $target_controller);
 
             $sub_url[] = $target_controller;
 
-            if(!empty($target_subject)){
+            if (!empty($target_subject)) {
 
                 $ctype = $subj_controller->getContentTypeForModeration($target_subject);
 
-                if($ctype){
+                if ($ctype) {
                     $target_subject = $ctype['name'];
                 } else {
                     return cmsCore::error404();
                 }
 
                 $this->model->filterEqual('target_subject', $target_subject);
 
                 $sub_url[] = $target_subject;
 
-                if($ctype){
+                if ($ctype) {
                     $additional_h1[] = $ctype['title'];
                 }
 
-                if(!empty($target_id)){
+                if (!empty($target_id)) {
 
                     $this->model->filterEqual('target_id', $target_id);
 
                     $sub_url[] = $target_id;
 
                     $this->model->lockFilters();
 
-                        $item = $this->model->getItem('moderators_logs', function ($item, $model){
-                            $item['data'] = cmsModel::yamlToArray($item['data']);
-                            return $item;
-                        });
+                    $item = $this->model->getItem('moderators_logs', function ($item, $model) {
+                        $item['data'] = cmsModel::yamlToArray($item['data']);
+                        return $item;
+                    });
 
-                        if($item){
-                            $additional_h1[] = $item['data']['title'];
-                        }
+                    if ($item) {
+                        $additional_h1[] = $item['data']['title'];
+                    }
 
                     $this->model->unlockFilters();
-
                 }
-
             }
-
         }
 
-        if(!empty($moderator_id)){
+        if (!empty($moderator_id)) {
 
             $this->model->filterEqual('moderator_id', $moderator_id);
 
-            if(count($sub_url) == 3){
+            if (count($sub_url) == 3) {
                 $sub_url[] = $moderator_id;
-            } elseif(count($sub_url) == 2){
+            } elseif (count($sub_url) == 2) {
                 $sub_url[] = 0;
                 $sub_url[] = $moderator_id;
-            } elseif(count($sub_url) == 1){
+            } elseif (count($sub_url) == 1) {
                 $sub_url[] = 0;
                 $sub_url[] = 0;
                 $sub_url[] = $moderator_id;
             } else {
                 $sub_url[] = 0;
@@ -105,69 +101,59 @@ class actionModerationLogs extends cmsAction {
                 $sub_url[] = $moderator_id;
             }
 
             $user = cmsCore::getModel('users')->getuser($moderator_id);
 
-            if($user){
+            if ($user) {
                 $additional_h1[] = $user['nickname'];
             }
-
         }
 
         if ($this->request->isAjax()) {
 
-            $filter     = array();
+            $filter     = [];
             $filter_str = $this->request->get('filter', '');
 
-            if ($filter_str){
+            if ($filter_str) {
                 parse_str($filter_str, $filter);
-                $this->model->applyGridFilter($grid, $filter);
+                $grid->applyGridFilter($this->model, $filter, 'moderators_logs');
             }
 
             $total = $this->model->getCount('moderators_logs');
-            $perpage = isset($filter['perpage']) ? $filter['perpage'] : admin::perpage;
-            $pages = ceil($total / $perpage);
 
             $this->model->joinUserLeft('moderator_id');
 
-            $data = $this->model->get('moderators_logs', function ($item, $model) use($subj_controller){
+            $data = $this->model->get('moderators_logs', function ($item, $model) use ($subj_controller) {
 
                 $item['data'] = cmsModel::yamlToArray($item['data']);
 
-                $item['controller_title'] = string_lang($item['target_controller'].'_CONTROLLER');
+                $item['controller_title'] = string_lang($item['target_controller'] . '_CONTROLLER');
 
                 $item['subject_title'] = $item['controller_title'];
 
-                if($subj_controller !== null){
+                if ($subj_controller !== null) {
 
                     $ctype = $subj_controller->getContentTypeForModeration($item['target_subject']);
 
                     $item['subject_title'] = $ctype['title'];
-
                 }
 
                 return $item;
+            }) ?: [];
 
-            });
-
-            $this->cms_template->renderGridRowsJSON($grid, $data, $total, $pages);
-
-            $this->halt();
-
+            return $this->cms_template->renderJSON($grid->makeGridRows($data, $total));
         }
 
-        if($additional_h1){
+        if ($additional_h1) {
             $this->cms_template->setPageH1($additional_h1);
         }
 
         $this->model->resetFilters();
 
-		return $this->cms_template->render('backend/logs', array(
+        return $this->cms_template->render('backend/logs', [
             'grid'      => $grid,
             'sub_url'   => $sub_url,
             'url_query' => $url_query,
-            'url'       => $url.($sub_url ? '/'.implode('/', $sub_url) : '').(($action > -1) ? '?'.http_build_query($url_query) : '')
-        ));
-
+            'url'       => $url . ($sub_url ? '/' . implode('/', $sub_url) : '') . (($action > -1) ? '?' . http_build_query($url_query) : '')
+        ]);
     }
-
 }
diff --git a/system/core/grid.php b/system/core/grid.php
index 92d9e2282..c9880dbca 100755
--- a/system/core/grid.php
+++ b/system/core/grid.php
@@ -237,11 +237,11 @@ class cmsGrid {
 
         $grid = call_user_func_array($grid_func_name, $args);
 
         foreach ($grid as $key => $data) {
 
-            $this->grid[$key] = array_merge(($this->grid[$key] ?? []), $data);
+            $this->grid[$key] = is_array($data) ? array_merge(($this->grid[$key] ?? []), $data) : $data;
         }
 
         // Фильтр по умолчанию
         $this->setDefaultFilter();
 
@@ -427,13 +427,14 @@ class cmsGrid {
     /**
      * Применяет фильтр к модели выборки
      *
      * @param cmsModel $model Объект модели, где выбираем записи
      * @param array $filter Массив фильтрации
+     * @param array $table_name Таблица, где ищем поля фильтра
      * @return cmsModel
      */
-    public function applyGridFilter(cmsModel $model, $filter) {
+    public function applyGridFilter(cmsModel $model, $filter, $table_name) {
 
         // применяем сортировку
         if (!empty($filter['order_by']) && !empty($filter['order_to'])) {
 
             // Есть ли вообще такой столбец
@@ -537,13 +538,18 @@ class cmsGrid {
         $this->grid['filter'] = array_merge($this->grid['filter'], $filter);
 
         // Дополнительный фильтр
         if (!empty($filter['advanced_filter']) && is_string($filter['advanced_filter'])) {
 
+            $dataset_filters = [];
+
             parse_str($filter['advanced_filter'], $dataset_filters);
 
-            $model->applyDatasetFilters($dataset_filters);
+            if (!$model->applyDatasetFilters($dataset_filters, true, [], $table_name)) {
+
+                $this->grid['filter']['advanced_filter'] = '';
+            }
         }
 
         return $model;
     }
 
diff --git a/system/core/model.php b/system/core/model.php
index 9c228e39d..f77e94c06 100755
--- a/system/core/model.php
+++ b/system/core/model.php
@@ -1194,41 +1194,63 @@ class cmsModel {
 
     public function filterOnlineUsers() {
         return $this->filterNotNull('online.user_id')->filterTimestampYounger('online.date_created', cmsUser::USER_ONLINE_INTERVAL, 'SECOND');
     }
 
-    public function applyDatasetFilters($dataset, $ignore_sorting = false, $allowed_fields = []) {
+    /**
+     * Применяет набор фильтров из массива
+     *
+     * @param array $dataset Массив фильтров/сортировки
+     * @param boolean $only_filters Применять только фильтры
+     * @param array $allowed_fields Разрешённые поля для фильтрации
+     * @param string $table_name Имя таблицы: если передано, все поля проверяются на доступность в ней
+     * @return boolean true, если что-либо применилось, false, если ничего
+     */
+    public function applyDatasetFilters($dataset, $only_filters = false, $allowed_fields = [], $table_name = '') {
 
-        if (!empty($dataset['filters'])) {
+        $success = false;
+
+        if (!empty($dataset['filters']) && is_array($dataset['filters'])) {
 
             foreach ($dataset['filters'] as $filter) {
 
+                // Небольшая валидация
+                if (
+                    empty($filter['field']) || !is_string($filter['field']) ||
+                    empty($filter['condition'] || !is_string($filter['condition'])) ||
+                    !array_key_exists('value', $filter) || (!is_string($filter['value']) && $filter['condition'] !== 'in')
+                    ) {
+                    continue;
+                }
+
+                // Есть ли такое поле в таблице
+                if ($table_name && !$this->db->isFieldExists($table_name, $filter['field'])) {
+                    continue;
+                }
+
                 // Если заданы разрешенные поля, проверяем
                 // валидация
                 if ($allowed_fields && !in_array($filter['field'], $allowed_fields, true)) {
                     continue;
                 }
 
-                if (isset($filter['callback']) && is_callable($filter['callback'])) {
+                // Таблица передаётся, когда field в $dataset могут быть переданы любые
+                if (!$table_name && isset($filter['callback']) && is_callable($filter['callback'])) {
                     $filter['callback']($this, $dataset);
                     continue;
                 }
 
-                if (!isset($filter['value'])) {
-                    continue;
-                }
                 if (($filter['value'] === '') && !in_array($filter['condition'], ['nn', 'ni'])) {
                     continue;
                 }
-                if (empty($filter['condition'])) {
-                    continue;
-                }
 
                 if ($filter['value'] !== '' && !is_array($filter['value'])) {
                     $filter['value'] = string_replace_user_properties($filter['value']);
                 }
 
+                $success = true;
+
                 switch ($filter['condition']) {
 
                     // общие условия
                     case 'eq': $this->filterEqual($filter['field'], $filter['value']);
                         break;
@@ -1270,19 +1292,25 @@ class cmsModel {
                         break;
                 }
             }
         }
 
-        if (!empty($dataset['sorting']) && !$ignore_sorting) {
+        if (!empty($dataset['sorting']) && !$only_filters) {
+
+            $success = true;
+
             $this->orderByList($dataset['sorting']);
         }
 
-        if (!empty($dataset['index'])) {
+        if (!empty($dataset['index']) && !$only_filters) {
+
+            $success = true;
+
             $this->forceIndex($dataset['index'], 2);
         }
 
-        return true;
+        return $success;
     }
 
     /**
      * Выборка по списку полей
      *
@@ -2249,18 +2277,20 @@ class cmsModel {
 //============================================================================//
 
     /**
      * Применяет к модели фильтры, переданные из просмотра
      * таблицы со списком записей
+     * Метод совместимости, не используйте его
      *
      * @param cmsGrid $grid Объект грида
      * @param array $filter
+     * @param string $table_name
      * @return $this
      */
-    public function applyGridFilter(cmsGrid $grid, $filter) {
+    public function applyGridFilter(cmsGrid $grid, $filter, $table_name = '') {
 
-        $grid->applyGridFilter($this, $filter);
+        $grid->applyGridFilter($this, $filter, $table_name);
 
         return $this;
     }
 
 //============================================================================//
diff --git a/system/traits/controllers/actions/listgrid.php b/system/traits/controllers/actions/listgrid.php
index 2f1c9ab86..61b0e7cda 100644
--- a/system/traits/controllers/actions/listgrid.php
+++ b/system/traits/controllers/actions/listgrid.php
@@ -335,11 +335,11 @@ trait listgrid {
 
                 if ($this->filter_callback) {
                     $filter = call_user_func_array($this->filter_callback, [$filter]);
                 }
 
-                $this->grid->applyGridFilter($this->model, $filter);
+                $this->grid->applyGridFilter($this->model, $filter, $this->table_name);
             }
         }
 
         if($this->list_callback){
             $this->model = call_user_func_array($this->list_callback, [$this->model]);
diff --git a/update/current/manifest.en.ini b/update/current/manifest.en.ini
index f668ec90b..d6e30a07e 100755
--- a/update/current/manifest.en.ini
+++ b/update/current/manifest.en.ini
@@ -7,11 +7,11 @@ type = "system"
 
 [version]
 major = "2"
 minor = "16"
 build = "1"
-date  = "20230613"
+date  = "20230912"
 
 [depends]
 core = "2.16.0"
 
 [author]
diff --git a/update/current/manifest.ru.ini b/update/current/manifest.ru.ini
index 5f74e4658..95b465998 100755
--- a/update/current/manifest.ru.ini
+++ b/update/current/manifest.ru.ini
@@ -7,11 +7,11 @@ type = "system"
 
 [version]
 major = "2"
 minor = "16"
 build = "1"
-date  = "20230613"
+date  = "20230912"
 
 [depends]
 core = "2.16.0"
 
 [author]

Vulnerable code: date = 20230613
            $ctrl_file = $this->cms_config->root_path . 'system/controllers/'.$controller_name.'/frontend.php';
            if(!is_readable($ctrl_file)){ continue; }
            if (!$hooks) { continue; }
                if(empty($hook_object->disallow_event_db_register)){
    public function run($target_controller = null, $target_subject = null, $target_id = null, $moderator_id = null){
        $url           = href_to($this->root_url, 'logs');
        $sub_url       = array();
        $url_query     = array();
        $additional_h1 = array();
        $action = $this->request->get('action', -1);
        if($action > -1){
            if($only_to_delete){

            $additional_h1[] = string_lang('LANG_MODERATION_ACTION_'.$action);

        if(!empty($target_controller)){
            if(!empty($target_subject)){
                if($ctype){
                if($ctype){
                if(!empty($target_id)){
                        $item = $this->model->getItem('moderators_logs', function ($item, $model){
                            $item['data'] = cmsModel::yamlToArray($item['data']);
                            return $item;
                        });
                        if($item){
                            $additional_h1[] = $item['data']['title'];
                        }



        if(!empty($moderator_id)){
            if(count($sub_url) == 3){
            } elseif(count($sub_url) == 2){
            } elseif(count($sub_url) == 1){
            if($user){

            $filter     = array();
            if ($filter_str){
                $this->model->applyGridFilter($grid, $filter);
            $perpage = isset($filter['perpage']) ? $filter['perpage'] : admin::perpage;
            $pages = ceil($total / $perpage);
            $data = $this->model->get('moderators_logs', function ($item, $model) use($subj_controller){
                $item['controller_title'] = string_lang($item['target_controller'].'_CONTROLLER');
                if($subj_controller !== null){

            });

            $this->cms_template->renderGridRowsJSON($grid, $data, $total, $pages);

            $this->halt();

        if($additional_h1){
		return $this->cms_template->render('backend/logs', array(
            'url'       => $url.($sub_url ? '/'.implode('/', $sub_url) : '').(($action > -1) ? '?'.http_build_query($url_query) : '')
        ));


            $this->grid[$key] = array_merge(($this->grid[$key] ?? []), $data);
    public function applyGridFilter(cmsModel $model, $filter) {
            $model->applyDatasetFilters($dataset_filters);
    public function applyDatasetFilters($dataset, $ignore_sorting = false, $allowed_fields = []) {
        if (!empty($dataset['filters'])) {
                if (isset($filter['callback']) && is_callable($filter['callback'])) {
                if (!isset($filter['value'])) {
                    continue;
                }
                if (empty($filter['condition'])) {
                    continue;
                }
        if (!empty($dataset['sorting']) && !$ignore_sorting) {
        if (!empty($dataset['index'])) {
        return true;
    public function applyGridFilter(cmsGrid $grid, $filter) {
        $grid->applyGridFilter($this, $filter);
                $this->grid->applyGridFilter($this->model, $filter);
date  = "20230613"
date  = "20230613"
Fixed code: date = 20230912
            if (!cmsController::enabled($controller_name)) {
                continue;
            }

            $ctrl_file = $this->cms_config->root_path . 'system/controllers/' . $controller_name . '/frontend.php';
            if (!is_readable($ctrl_file)) {
                continue;
            }
            if (!$hooks) {
                continue;
            }
                if (empty($hook_object->disallow_event_db_register)) {
    public function run($target_controller = null, $target_subject = null, $target_id = null, $moderator_id = null) {
        $url             = href_to($this->root_url, 'logs');
        $sub_url         = [];
        $url_query       = [];
        $additional_h1   = [];
        $action         = $this->request->get('action', -1);

        if ($action > -1) {
            if ($only_to_delete) {
            $additional_h1[] = string_lang('LANG_MODERATION_ACTION_' . $action);
        if (!empty($target_controller)) {
            if (!empty($target_subject)) {
                if ($ctype) {
                if ($ctype) {
                if (!empty($target_id)) {
                    $item = $this->model->getItem('moderators_logs', function ($item, $model) {
                        $item['data'] = cmsModel::yamlToArray($item['data']);
                        return $item;
                    });
                    if ($item) {
                        $additional_h1[] = $item['data']['title'];
                    }
        if (!empty($moderator_id)) {
            if (count($sub_url) == 3) {
            } elseif (count($sub_url) == 2) {
            } elseif (count($sub_url) == 1) {
            if ($user) {
            $filter     = [];
            if ($filter_str) {
                $grid->applyGridFilter($this->model, $filter, 'moderators_logs');
            $data = $this->model->get('moderators_logs', function ($item, $model) use ($subj_controller) {
                $item['controller_title'] = string_lang($item['target_controller'] . '_CONTROLLER');
                if ($subj_controller !== null) {
            }) ?: [];
            return $this->cms_template->renderJSON($grid->makeGridRows($data, $total));
        if ($additional_h1) {
        return $this->cms_template->render('backend/logs', [
            'url'       => $url . ($sub_url ? '/' . implode('/', $sub_url) : '') . (($action > -1) ? '?' . http_build_query($url_query) : '')
        ]);
            $this->grid[$key] = is_array($data) ? array_merge(($this->grid[$key] ?? []), $data) : $data;
     * @param array $table_name Таблица, где ищем поля фильтра
    public function applyGridFilter(cmsModel $model, $filter, $table_name) {
            $dataset_filters = [];

            if (!$model->applyDatasetFilters($dataset_filters, true, [], $table_name)) {

                $this->grid['filter']['advanced_filter'] = '';
            }
    /**
     * Применяет набор фильтров из массива
     *
     * @param array $dataset Массив фильтров/сортировки
     * @param boolean $only_filters Применять только фильтры
     * @param array $allowed_fields Разрешённые поля для фильтрации
     * @param string $table_name Имя таблицы: если передано, все поля проверяются на доступность в ней
     * @return boolean true, если что-либо применилось, false, если ничего
     */
    public function applyDatasetFilters($dataset, $only_filters = false, $allowed_fields = [], $table_name = '') {
        $success = false;

        if (!empty($dataset['filters']) && is_array($dataset['filters'])) {
                // Небольшая валидация
                if (
                    empty($filter['field']) || !is_string($filter['field']) ||
                    empty($filter['condition'] || !is_string($filter['condition'])) ||
                    !array_key_exists('value', $filter) || (!is_string($filter['value']) && $filter['condition'] !== 'in')
                    ) {
                    continue;
                }

                // Есть ли такое поле в таблице
                if ($table_name && !$this->db->isFieldExists($table_name, $filter['field'])) {
                    continue;
                }

                // Таблица передаётся, когда field в $dataset могут быть переданы любые
                if (!$table_name && isset($filter['callback']) && is_callable($filter['callback'])) {
                $success = true;

        if (!empty($dataset['sorting']) && !$only_filters) {

            $success = true;

        if (!empty($dataset['index']) && !$only_filters) {

            $success = true;

        return $success;
     * Метод совместимости, не используйте его
     * @param string $table_name
    public function applyGridFilter(cmsGrid $grid, $filter, $table_name = '') {
        $grid->applyGridFilter($this, $filter, $table_name);
                $this->grid->applyGridFilter($this->model, $filter, $this->table_name);
date  = "20230912"
date  = "20230912"