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

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

И все таки правильно говорят, а на некторых форумах (особенно 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. Ну на этом достаточно слов, приведу сам код фильтра — он  совсем простой, так что думаю проблем быть не должно.

  /**
   *  @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('&amp;','&lt;','&gt;'), array('&amp;amp;','&amp;lt;','&amp;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 для этих целей, но «родная» возможность сделать это была бы лучшим вариантом.

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

Ищите разработчиков на Yii ?

Мы решим Ваши задачи!

Скачать xssfilter

Читайте еще:

  • ayn

    этот фреймворк Yii чертовски занимательная вещичка
    сейчас тоже активно юзаю
    вопросов конечно много пока
    вот сейча спро фильтры узнал много интересного и полезного

  • kosenka

    Хорошо бы в фильтр добавить исключение для actions.
    Например, мне надо фильтровать все actions, кроме «этого» и «этого».

  • kosenka

    вообщем, покопался в коде и сделал так:


    public $x_actions = '';//исключаемые экшены

    // если экшн найдется в "исключаемых" - просто выходим из фильтра
    if(in_array($filterChain->action->id,explode(',',$this->x_actions)))
    {
    return true;
    }

    Не знаю, правильно ли так делать, но у меня работает.

    • xoma

      Да, хорошее предложение!

  • Утба, спасибо за замечание. Я тоже об этом думал http://allframeworks.ru/blog/Yii/20.html =) Исправлю в ближайшее время.

  • Aleksey Belashov

    Небольшая ошибка: в инструкции сказано ‘application.filters.XssFilter’ а архиве класс YXssFilter

  • Юрий

    Принимай очередной лайк! Спасибо за либу