CVE-2023-4928 (published 2023-09-13T01:15Z)

 SQL Injection in GitHub repository instantsoft/icms2 prior to 2.16.1.

CVSS v3 base score: 7.2, HIGH (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-89
cwe_name: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
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.
extended_description: 
            
            
         
         

Fix information:
repo_url: https://github.com/instantsoft/icms2
commit_details: repo_url: https://github.com/instantsoft/icms2
author: fuzegit
author_date: 2023-09-12 18:12:27+03:00
msg: Fix SQL Injection in datagrid advanced filter
repository: repo_url: https://github.com/instantsoft/icms2
repo_name: instantsoft/icms2
description: Self-hosted Site Management System
repo_language: PHP
owner: instantsoft
file_changes: filename: version.ini
change_type: ModificationType.MODIFY
diff: @@ -2,4 +2,4 @@
 major = 2
 minor = 16
 build = 1
-date = 20230613
\ No newline at end of file
+date = 20230912
\ No newline at end of file

programming_language: Haskell
filename: frontend.php
change_type: ModificationType.MODIFY
diff: @@ -707,11 +707,19 @@ class admin extends cmsFrontend {
 
         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);
 
@@ -724,7 +732,7 @@ class admin extends cmsFrontend {
                 // Некоторые хуки не требуют регистрации в базе данных,
                 // Например, хуки для 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;
 

programming_language: PHP
filename: logs.php
change_type: ModificationType.MODIFY
diff: @@ -2,39 +2,38 @@
 
 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);
 
@@ -42,11 +41,11 @@ class actionModerationLogs extends cmsAction {
 
             $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();
@@ -56,11 +55,11 @@ class actionModerationLogs extends cmsAction {
 
                 $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);
 
@@ -68,33 +67,30 @@ class actionModerationLogs extends cmsAction {
 
                     $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;
@@ -107,67 +103,57 @@ class actionModerationLogs extends cmsAction {
 
             $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) : '')
+        ]);
     }
-
 }

programming_language: PHP
filename: grid.php
change_type: ModificationType.MODIFY
diff: @@ -239,7 +239,7 @@ class cmsGrid {
 
         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;
         }
 
         // Фильтр по умолчанию
@@ -429,9 +429,10 @@ 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'])) {
@@ -539,9 +540,14 @@ class cmsGrid {
         // Дополнительный фильтр
         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;

programming_language: PHP
filename: model.php
change_type: ModificationType.MODIFY
diff: @@ -1196,37 +1196,59 @@ class cmsModel {
         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']) {
 
                     // общие условия
@@ -1272,15 +1294,21 @@ class cmsModel {
             }
         }
 
-        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;
     }
 
     /**
@@ -2251,14 +2279,16 @@ 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;
     }

programming_language: PHP
filename: listgrid.php
change_type: ModificationType.MODIFY
diff: @@ -337,7 +337,7 @@ trait listgrid {
                     $filter = call_user_func_array($this->filter_callback, [$filter]);
                 }
 
-                $this->grid->applyGridFilter($this->model, $filter);
+                $this->grid->applyGridFilter($this->model, $filter, $this->table_name);
             }
         }
 

programming_language: PHP
filename: manifest.en.ini
change_type: ModificationType.MODIFY
diff: @@ -9,7 +9,7 @@ type = "system"
 major = "2"
 minor = "16"
 build = "1"
-date  = "20230613"
+date  = "20230912"
 
 [depends]
 core = "2.16.0"

programming_language: CoffeeScript
filename: manifest.ru.ini
change_type: ModificationType.MODIFY
diff: @@ -9,7 +9,7 @@ type = "system"
 major = "2"
 minor = "16"
 build = "1"
-date  = "20230613"
+date  = "20230912"
 
 [depends]
 core = "2.16.0"

programming_language: CoffeeScript