Yii, пишем фильтр для предотвращения XSS-атак

Yiiframework 2 июля 2009 г., 17:11

Начну с небольшого отступления.

И все таки правильно говорят, а на некторых форумах (особенно UNIX-овых), прямо кричат — RTFM! Кто не понял очем идет речь — RTFM в переводе означает «читай эту чертову документацию!». Это все я собственно вот к чему: изучая и что-то пытаясь написать на фреймворке Yii, возникла задача фильтрации входных данных от различного рода «зловредных» символов (аля XSS-атака) и первое что пришло в голову — это написать свой фильтр (что я все таки и сделал), однако creocoder, на форуме Yii, совершенно спрпаведливо заметил, что не зачем изобретать велосипед, все уже есть готовое, необходимо только RTFM! Речь шла о классе CHtmlPurifier, который является оберткой для библиотеки HTML Purifier, и выполняет все те функции, которые мне необходимы (правда я так и не попробывал его в действии, может и зря конечно). Но раз уж я начал писать свой фильтр — решил все таки это дело завершить, да и просто написать статью о фильтрах в Yii.

И так!
Фильтры — фрагменты кода, которые могут быть выполнены до и\или после выполнения экшена контроллера. Фильтры, при необходимости, могут не допустить выполнения запрошенного экшена.

Фильтры могут быть как методами текущего контроллера, так и отдельными классами — что позволяет повторно их использовать. Если фильтр реализуется как метод класса, он должен иметь префикс «filter».

Пример:


      public function filterAccessControl()   
      {  
       .......   
      }


Фильтр, реализованный в виде отдельного класса, должен быть наследником класса CFilter.

Пример:


      class XssFilter extends CFilter
      {   
       // код который выполнится ДО выполнения экшена   
       public function preFilter()  
       {   
        .......
       }

       // код который выполнится ПОСЛЕ выполнения экшена
       public function postFilter()  
       {  
       .......
       }  
      }

Для активации фильтров, необходимо в контроллере переопределить метод filters, который должен вернуть массив всех фильтров для данного контроллера (или его отдельных экшенов).

Пример:

      public function filters()   
      {  
       return array(   
                 'accessControl', 
                 array(  
                   'application.filters.XssFilter', 
                   'clean' => 'all' 
                )
           ); 
      }


В этом примере 'accessControl' — фильтр, реализованный как метод контроллера, а 'application.filters.XssFilter' — фильтр, реализованный в виде отдельного класса, который хранится в каталоге /protected/filters/. 'clean' — устанавливаем свойство фильтра.

Это была краткая справка по фильтрам в Yii, более подробно можно почитать тут.

Теперь непосредственно приступим к реализации нашего фильтра. Функцию очистки данных, которая и выполняет всю работу — я взял из фреймворка Kohana. Ну на этом достаточно слов, приведу сам код фильтра — он совсем простой, так что думаю проблем быть не должно.


<?php
  /**
   *  @author  Opeykin A. <andrey.opeykin.ru> <aopeykin@gmail.com>
   *  @version 0.0.1
   *  @package filters
   *
   * Фильтр предназначен для фильтрации входных данных, c целью предотвратить xss атаки.
   * Для фильтрации используются регулярные выражения из фреймворка Kohana 2.3.1
   * @example
   *
   *  public function filters()
   *  {
   *         return array(
   *                 array('application.filters.XssFilter',
   *                       'clean' => 'all'
   *                 )
   *         );
   *
   *   }
   *
   *   В качетве параметра 'clean' могут быть:
   *  - 'all' - фильтруются GET,POST,COOKIE,FILES массивы;
   *  - '*'   - аналог ALL;
   *  - так же возможно сочетание любых из параметров, например GET,COOKIE или POST,FILES  
   */

class XssFilter extends CFilter
{

        public  $clean = 'all';       

        protected function preFilter($filterChain)
        {              
                $this->clean  = trim(strtoupper($this->clean));
                $data = array(
                         'GET'    => &$_GET,
                         'POST'   => &$_POST,
                         'COOKIE' => &$_COOKIE,
                         'FILES'  => &$_FILES
                );
                
                if($this->clean === 'ALL' || $this->clean === '*')
                {                        
                        $this->clean = 'GET,POST,COOKIE,FILES';
                }

                $dataForClean = split(',',$this->clean);
                if(count($dataForClean))
                {                 
                        foreach ($dataForClean as $key => $value)
                        {                 
                                if(isset ($data[$value]) && count($data[$value]))
                                {
                                        $this->doXssClean($data[$value]);
                                }
                        }
                }

              return true;
        }
        

        protected function postFilter($filterChain)
        {
                // logic being applied after the action is executed
        }


        private function doXssClean(&$data)
        {
                if(is_array($data) && count($data))
                {                       
                       foreach($data as $k => $v)
                       {
                               $data[$k] = $this->doXssClean($v);
                       }
                       return $data;
                }

                if(trim($data) === '')
                {
                        return $data;
                }

                // xss_clean function from Kohana framework 2.3.1                
                $data = str_replace(array('&','<','>'), array('&amp;','&lt;','&gt;'), $data);
                $data = preg_replace('/(&#*\w+)[\x00-\x20]+;/u', '$1;', $data);
                $data = preg_replace('/(&#x*[0-9A-F]+);*/iu', '$1;', $data);
                $data = html_entity_decode($data, ENT_COMPAT, 'UTF-8');
                // Remove any attribute starting with "on" or xmlns
                $data = preg_replace('#(<[^>]+?[\x00-\x20"\'])(?:on|xmlns)[^>]*+>#iu', '$1>', $data);
                // Remove javascript: and vbscript: protocols
                $data = preg_replace('#([a-z]*)[\x00-\x20]*=[\x00-\x20]*([`\'"]*)[\x00-\x20]*j[\x00-\x20]*a[\x00-\x20]*v[\x00-\x20]*a[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2nojavascript...', $data);
                $data = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*v[\x00-\x20]*b[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2novbscript...', $data);
                $data = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*-moz-binding[\x00-\x20]*:#u', '$1=$2nomozbinding...', $data);
                // Only works in IE: <span style="width: expression(alert('Ping!'));"></span>
                $data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?expression[\x00-\x20]*\([^>]*+>#i', '$1>', $data);
                $data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?behaviour[\x00-\x20]*\([^>]*+>#i', '$1>', $data);
                $data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:*[^>]*+>#iu', '$1>', $data);
                // Remove namespaced elements (we do not need them)
                $data = preg_replace('#</*\w+:\w[^>]*+>#i', '', $data);
                do
                {
                        // Remove really unwanted tags
                        $old_data = $data;
                        $data = preg_replace('#</*(?:applet|b(?:ase|gsound|link)|embed|frame(?:set)?|i(?:frame|layer)|l(?:ayer|ink)|meta|object|s(?:cript|tyle)|title|xml)[^>]*+>#i', '', $data);
                }
                while ($old_data !== $data);                
                return $data;
        }

}
?>



Я совсем немного протестировал это фильтр — на первый взгляд — все работает!

Любые замечания и комментарии приветствуются!

Хочу добавить свои замечания к реализации фильтров в Yii…

Мне кажется было удобно иметь метод, который вызывается перед выполнением preFilter и postFilter, например init () — который выполняет инициализацию фильтра, при этом в нем должны быть доступны параметры, передавемые в фильтр из контроллера (по этой причине невозможно использовать __construct). Конечно можно расширить CFilter для этих целей, но «родная» возможность сделать это была бы лучшим вариантом.

Надеюсь, описанный материал окажется полезен!

Оригинал статьи
Скачать XSS-фильтр для Yii

Юпи! — CMS на Yii – http://yupe.ru

Исходный код – https://github.com/yupe/yupe

Присоединяйтесь!



Комментарии 5

Bethrezen
Bethrezen
Надо будет как нибудь затестить твой фильтр
xoma
xoma
Буду благодарен )
xoma
xoma
Весрию 0.0.2 можно взять из SVN code.google.com/p/xomaprojects/source/browse/trunk/yii/filters/YXssFilter.php — Добавлена возможность вырезать или экранировать html-теги
DARX
DARX
Привет Мне кажется, что неправильно выбрана сама реализация защиты от XSS. По сути, она представляет собой очистку входных данных от «опасных» символов. Так зачем использовать фильтры в контексте Yii? Они же всего-навсего либо запрещают, либо разрешают выполнение тех или иных экшенов в зависимости от параметров… Намного грамотнее использовать хелперы, на вход которых подается «опасная» строка, а на выходе — «безопасная». А это уже и реализовано в CHtmlPurifier, как и было отмечено. Имхо.
xoma
xoma
Я не думаю что действие фильтров, ограничивается лишь запретом и разрешением выполнения того или иного экшена. Фильтры могут использоваться, к примеру, для измерения производительности, а так же для первоначальной обработки запроса/ответа. Используя хелпер для очистки данных, можно очень легко забыть «профильтровать» ту или иную строку (например добавился новый параметр на форму — необходимо помнить что он опасен), фильтр ставится один раз и про него можно забыть ) Имхо, конечно!
Пожалуйста, авторизуйтесь или зарегистрируйтесь для комментирования!