• Блог
  • Фильтрация входящих данных

В Москве прошёл митап, на котором Евгений Борисов осветил аспект безопасности MODX. Его доклад можно найти в сообществе. И вот, прочитав его, некоторые разработчики (в том числе и я), обнаружили, что чересчур доверяли MODX в плане фильтрации данных. Правда некоторые вообще ничего об этом не знают, но это их не оправдывает. В общем, одна из проблем фильтрации, которую он показал, работает на большинстве сайтов. А вот почему, давайте пробежимся.

Когда в обычном режиме поступает запрос, MODX запускает метод protect, который фильтрует некоторые данные запроса ($_SERVER["HTTP_USER_AGENT"], $_SERVER["HTTP_REFERER"], $_SERVER["QUERY_STRING"]). Дальше запрос передаётся классу modRequest, который фильтрует входящие данные в массивах $_GET, $_POST, $_REQUEST и $_COOKIE. Для фильтра он использует вот такой массив:

$sanitizePatterns = array(
    'scripts'  => '@<script[^>]*?>.*?</script>@si',
    'entities' => '@&#(\d+);@',
    'tags1'    => '@\[\[(.*?)\]\]@si',
    'tags2'    => '@(\[\[|\]\])@si',
);

Как видим, из запроса вырезаются тег script, коды символов и теги MODX. Т.е. если указать в запросе тег [[someTag]], то он в массиве данных будет удалён. Но Евгений показал, как этот фильтр можно легко обойти. Достаточно добавить теги HTML.

[<b></b>[someTag]<b></b>]

Такая строка под регулярку не попадает. И если эти данные выводятся на странице, то, как говорится, «Милости просим». И это как бы не косяк MODX. Хотя к настройке allow_tags_in_post можно было бы добавить настройку для вырезания тегов HTML из запросов. Но пока решение этой задачи нужно будет взять в свои руки. Для этого придётся создать плагин и в нём самостоятельно фильтровать данные.

Первое, что можно сделать — удалить теги HTML. В этом случае Женин пример уже работать не будет. MODX сам справится и удалит свои теги из запроса. Второй вариант — кодировать скобки как советовал Евгений. В этом случае MODX их, конечно, не вырежет, но и парсить не будет.

Лично я выбрал оба варианта. Для удобства я добавил пару функций в библиотеку modHelperstag_encode() и tag_decode(), которые преобразуют и квадратные и фигурные скобки в соответствующие коды. Вот как это выглядит:

class webListener extends Middlewares\Listener
{
    public function OnMODXInit($before=true)
    {
        // Фильтруем скобки
        foreach ($_REQUEST as $key => &$value) {
            if (is_string($value)) {
                $value = tag_encode(strip_tags($value));  // Новая функция modHelpers
            }
        }
        foreach ($_POST as $key => &$value) {
            if (is_string($value)) {
                $value = tag_encode(strip_tags($value)); // Новая функция modHelpers
            }
        }
        foreach ($_GET as $key => &$value) {
            if (is_string($value)) {
                $value = tag_encode(strip_tags($value)); // Новая функция modHelpers
            }
        }
        foreach ($_COOKIE as $key => &$value) {
            if (is_string($value)) {
                $value = tag_encode(strip_tags($value)); // Новая функция modHelpers
            }
        }
    }

    public function OnWebPagePrerender()
    {
        // Чтобы на странице выводились не коды, возвращаем скобки.
        $output = &$this->modx->resource->_output;
        $output = htmlspecialchars_decode($output);
    }
}

Как видите, я использую компонент Middlewares вместо плагинов, но вы можете сделать классический плагин на события с названиями методов. Только у события OnMODXInit поставьте отрицательный приоритет, чтобы выполнялся раньше других плагинов.

Что важно ещё отметить. Я в предыдущей статье призывал разработчиков отказаться от использования массива $_REQUEST. Но многие разработчики игнорируют данную рекомендацию, чем упрощают жизнь всяким хулиганам (ибо вместо POST запроса можно слать GET, что проще). И поэтому не забывайте фильтровать все массивы с данными. Давайте я даже это выделю.

Важно!

Кроме $_GET и $_POST массивов обязательно фильтруйте и $_REQUEST!

Заключение

Если вы всё это будете фильтровать, то и все дополнения будут работать корректно. Но авторам дополнений, по-любому, нужно включать фильтрацию, а не полагаться на разработчика сайта.

И ещё нужно знать, что в API режиме (define('MODX_API_MODE', true)) обработчик запроса (класс modRequest) не запускается. Поэтому и описанной выше стандартной фильтрации MODX не будет.

П.С. Себе я новые функции modHelpers уже добавил, а пакет выпущу чуть позднее, так как нужно не только русскую документацию добавить, но и буржуйскую.

1   1832

Комментарии ()

  1. Сергей Шлоков 03 октября 2018, 17:52 # +1
    Женя посоветовал ещё и $_COOKIE фильтровать от греха подальше. Добавил в пример.
    1. Vladimir 04 декабря 2018, 12:05 # 0
      @telefeedbot сейчас а телеге (rss извещения) эту статью пометил обновленную.
      Не вижу, что-то дополнено?
      1. Сергей Шлоков 04 декабря 2018, 18:50 # 0
        Я тоже не вижу :)

        Видимо глюк рсс.
      2. Дмитрий 01 февраля 2019, 22:23 # 0
        Сергей, очень заинтересовала такая фильтрация данных.

        Но пока решение этой задачи нужно будет взять в свои руки. Для этого придётся создать плагин и в нём самостоятельно фильтровать данные.

        Первое, что можно сделать — удалить теги HTML. В этом случае Женин пример уже работать не будет. MODX сам справится и удалит свои теги из запроса. Второй вариант — кодировать скобки как советовал Евгений. В этом случае MODX их, конечно, не вырежет, но и парсить не будет.

        Лично я выбрал оба варианта.

        Поскольку не очень силен в php и недавно познакомился с MODX, не совсем понятно как это реализовать на практике. Если не затруднит, можешь привести примеры реализации с плагином и с использованием компонента Middlewares.
        Думаю для таких как я, новичков в MODX, будет очень полезно это узнать, да и полагаю, что количество использующих твои компоненты, в том числе modHelpers, возрастет.

        И поясни, таким способом будут фильтроваться все входящие данные, или еще что-то дополнительно нужно делать?

        Заранее, благодарю!
        1. Сергей Шлоков 02 февраля 2019, 10:25 # +1
          Дмитрий, если хочешь научиться, то начинай с начала, а не с конца. Прочитай документацию. По middlewares есть видео. Вообще middlewares требует определенных знаний php. Поэтому лучше освоить базовый функционал MODX. Создай плагин на событие OnHandleRequest и приоритетом -1000 и вставь в него код
          foreach ($_REQUEST as $key => &$value) {
          	if (is_string($value)) {
          		$value = tag_encode(strip_tags($value)); 
          	}
          }
          foreach ($_POST as $key => &$value) {
          	if (is_string($value)) {
          		$value = tag_encode(strip_tags($value)); 
          	}
          }
          foreach ($_GET as $key => &$value) {
          	if (is_string($value)) {
          		$value = tag_encode(strip_tags($value)); 
          	}
          }
          foreach ($_COOKIE as $key => &$value) {
          	if (is_string($value)) {
          		$value = tag_encode(strip_tags($value));
          	}
          }
          
          1. Дмитрий 02 февраля 2019, 15:11 # 0
            1. Сергей, в статье ты рекомендуешь события OnMODXInit:
            … но вы можете сделать классический плагин на события с названиями методов. Только у события OnMODXInit поставьте отрицательный приоритет, чтобы выполнялся раньше других плагинов.

            А в ответе на мой вопрос рекомендуешь создать плагин на событие OnHandleRequest, которое идет, как я понимаю, сразу за событием OnMODXInit
            Создай плагин на событие OnHandleRequest

            Так как все-таки правильнее?

            2. В статье есть такая функция:
            public function OnWebPagePrerender()
                {
                    // Чтобы на странице выводились не коды, возвращаем скобки.
                    $output = &$this->modx->resource->_output;
                    $output = htmlspecialchars_decode($output);
                }


            Нудна ли она в плагине?

            Например, сделал так:

            <?php
            
            switch ($modx->event->name) {
            	
                case 'OnHandleRequest':
            	
            		// Фильтруем скобки
            		foreach ($_REQUEST as $key => &$value) {
            			if (is_string($value)) {
            				$value = tag_encode(strip_tags($value)); 
            			}
            		}
            		foreach ($_POST as $key => &$value) {
            			if (is_string($value)) {
            				$value = tag_encode(strip_tags($value)); 
            			}
            		}
            		foreach ($_GET as $key => &$value) {
            			if (is_string($value)) {
            				$value = tag_encode(strip_tags($value)); 
            			}
            		}
            		foreach ($_COOKIE as $key => &$value) {
            			if (is_string($value)) {
            				$value = tag_encode(strip_tags($value));
            			}
            		}
            		
            	break;
            	
            	
            		
                case 'OnWebPagePrerender':
            	
                    // Чтобы на странице выводились не коды, возвращаем скобки.
                    $output = &$this->modx->resource->_output;
                    $output = htmlspecialchars_decode($output);
            				
            	break;
            		
            }

            Если это правильно, то на событиеOnWebPagePrerender тоже ставить "-1000"?

            Если неправильно, то подскажи как правильно.

            Заранее, благодарю!
            1. Сергей Шлоков 02 февраля 2019, 20:18 # +1
              OnMODXInit срабатывает при инициализации MODX. Т.е. в обычном режиме и в аякс.
              OnHandleRequest срабатывает только в обычном режиме. Для FormIt, по-моему, сработают оба события. Поэтому для тебя без разницы.
              Событие OnWebPagePrerender тоже можно включить. Приоритет не важен.
              1. Дмитрий 03 февраля 2019, 03:50 # 0
                Странное поведение плагина.
                Пробовал и OnHandleRequest и OnMODXInit.

                С событием OnHandleRequest плагин вообще не срабатывает, как в твоем варианте, так и в моем.

                С OnMODXInit работает, как в твоем, так и в моем варианте, НО:o
                если отправить форму, а потом в админке сохранить изменения в ЛЮБОМ плагине, а после перезагрузить эту страницу админки, или перейти на другую страницу админки, то весь код из сохраненного плагина удаляется, остается только
                <?php
                при этом в журнал летит ошибка:
                .../model/modx/processors/element/plugin/update.class.php : 68) PHP warning: Invalid argument supplied for foreach()

                Чтобы вставить код обратно в тот плагин, приходится перейти в плагин фильтрации, нажать кнопку «Сохранить», при этом код из плагина фильтрации также удаляется, после надо почистить кэш (удаляю папку с кэшем), после вставить опять код в плагин фильтрации, сохранить и только после этого становиться возможным вставить и сохранить код в первом плагине (по другому, если вставлять, то после сохранения код опять пропадает и летит еще одна указанная ошибка).

                Пропадает код также и в плагине фильтрации, если после отправки формы нажать в админке в плагине фильтрации кнопку «Сохранить»:o
                Как-то так, если ничего не упустил.

                Не подскажешь в чем может быть проблема?
                1. Сергей Шлоков 03 февраля 2019, 09:04 # 0
                  С событием OnHandleRequest плагин вообще не срабатывает, как в твоем варианте, так и в моем.
                  Самая частая ошибка — забывают отметить на вкладке событий соответствующие события.

                  Не подскажешь в чем может быть проблема?
                  Без понятия. Видимо что-то сломал.
                  1. Дмитрий 03 февраля 2019, 13:22 # 0
                    С событием OnHandleRequest плагин вообще не срабатывает, как в твоем варианте, так и в моем.
                    Самая частая ошибка — забывают отметить на вкладке событий соответствующие события.
                    Дело в том, что проверял несколько раз, сейчас еще раз проверил. Не срабатывает на событие OnHandleRequest

                    Не подскажешь в чем может быть проблема?
                    Без понятия. Видимо что-то сломал.

                    На двух сайтах проверил, поведение плагина одно и тоже. Может код для плагина должен быть немного другой, может что-то упустил?
                    1. Сергей Шлоков 03 февраля 2019, 14:45 # +1
                      Дело в том, что проверял несколько раз, сейчас еще раз проверил. Не срабатывает на событие OnHandleRequest
                      А форма аякс?

                      На двух сайтах проверил, поведение плагина одно и тоже. Может код для плагина должен быть немного другой, может что-то упустил?
                      Добавь в начале плагина
                      if ($modx->context->key == 'mgr') return;
                      
                      1. Дмитрий 03 февраля 2019, 15:29 # 0
                        А форма аякс?
                        Да, вот такой вывод
                        {$_modx->runSnippet('!AjaxForm@MyAjaxForm', [
                                    'snippet' => 'FormIt',
                                    'form' => '@FILE chunks/forms/contact_form.tpl',
                                    'hooks' => 'email,FormItSaveForm',
                                    'formName' => 'Форма со страницы контакты',			
                                    'formFields' => 'name,email,subject,message',			
                                    'fieldNames' => 'name==Имя,email==E-mail,subject==Тема вопроса,message==Текст сообщения',
                        	    
                                    'validate' => 'name:required:minLength=^3^, email:email:required, subject:required:minLength=^3^, message:required:minLength=^6^',	
                        			            		
                                    'emailTo' => 'name@mail.com',
                                    'emailTpl' => '@FILE chunks/email/contacts_email.tpl',
                                    'emailSubject' => 'Письмо со страницы контакты',
                                    'emailFromName' => 'От адвоката',
                                    'validationErrorMessage' => 'В форме содержатся ошибки!',
                                    'successMessage' => 'Форма успешно отправлена!'
                        ])}
                        

                        Добавь в начале плагина
                        if ($modx->context->key == 'mgr') return;

                        Да, так вроде все работает и не удаляется код. Проверю еще на других сайтах, отпишусь.

                        Огромное спасибо за твою работу по улучшению MODX и за оперативную помощь лично мне!
                        1. Сергей Шлоков 03 февраля 2019, 15:52 # +1
                          Для аякс запросов событие OnHandleRequest не срабатывает.
                          1. Дмитрий 03 февраля 2019, 17:53 # 0
                            понятно:)
                    2. Дмитрий 03 февраля 2019, 15:04 # 0
                      Проверил на третьем сайте.
                      Здесь вообще, создал плагин с событием OnMODXInit, выставил приоритет "-1000", вставил твой код, сохранил и сайт сломался — ошибка 500 на всем сайте, в том числе в админке.

                      Чтобы сайт заработал, пришлось из базы удалять код плагина и удалять папку cache из core.

                      Плагин с событием OnHandleRequest ничего ни на одном сайте не ломает, но и НЕ работает, скобки на коды не меняет.

                      Может кроме библиотеки modHelpers еще что-то твое нужно установить?
                      1. Сергей Шлоков 03 февраля 2019, 15:56 # +1
                        Выставь приоритет -100. Возможно твой плагин запускается раньше modHelpers.
                        Ошибку можно посмотреть в логах сервера.

                        Чтобы сайт заработал, пришлось из базы удалять код плагина и удалять папку cache из core.
                        На будущее. Достаточно было удалить код в файле плагина в папке core/cache/includes/elements/plugin/id.cache.php, где id — это id плагина.
                        1. Дмитрий 03 февраля 2019, 17:50 # 0
                          Благодарю, с приоритетом -100 заработало на всех сайтах. А то на одном после сохранения плагина во фронтэнде ошибка 500 была, сейчас все ок.

                          Подскажи еще следующее.
                          Например, входящие данные такие:
                          [<b></b>[*pagetitle]<b></b>]

                          С помощью первой части плагина $_GET, $_POST, $_REQUEST и $_COOKIE фильтруются от MODX тегов (плейсхолдеров), скобки заменяются на коды.

                          а с помощью вот этой части:
                          <?php
                          //................
                          
                              case 'OnWebPagePrerender':
                          	
                                  // Чтобы на странице выводились не коды, возвращаем скобки.
                                  $output = &$this->modx->resource->_output;
                                  $output = htmlspecialchars_decode($output);
                          				
                          	break;
                          		
                          }
                          возвращаются скобки и на сайте отображается плейсхолдер вот в таком виде — [[*pagetitle]] или выводится его содержимое (значение)?

                          Думаю, что в таком виде — [[*pagetitle]].

                          Например, в комментариях, если ввести
                          [<b></b>[*pagetitle]<b></b>]
                          То должно вывестись — [[*pagetitle]], а не значение (содержимое) этого плейсхолдера.

                          Правильно я понимаю?

                          К сожалению комментарии пока не сделал и проверить не могу, поэтому спрашиваю.
                          Может еще как-то можно проверить?
                          1. Сергей Шлоков 03 февраля 2019, 21:37 # 0
                            Тут всё зависит от компонентов сайта. Как правило, дополнения, которые выводят пользовательские данные, дополнительно кодируют спец. символы для безопасности. В этом случае из-за двойного преобразования скобки выведутся как
                            &#91;&#91;*pagetitle&#93;&#93;
                            Поэтому один раз нужно декодировать. Но это по необходимости.

          Вы должны авторизоваться, чтобы оставлять комментарии.

          Выделите опечатку и нажмите Ctrl + Enter, чтобы отправить сообщение об ошибке.