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

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

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

И так!

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

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

Пример:

  1. public function filterAccessControl()
  2. {
  3.  …….
  4. }

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

Пример:

  1. class XssFilter extends CFilter
  2. {
  3.    // код который выполнится <strong>до </strong>выполнения экшена
  4.  public function preFilter()
  5.  {
  6.   …….
  7.  }
  8.  // код который выполнится<strong> после</strong> выполнения экшена
  9.  public function postFilter()
  10.  {
  11.  …….
  12.  }
  13. }

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

Пример:

  1. public function filters()
  2. {
  3.  return array(
  4.            'accessControl',
  5.            array(
  6.              'application.filters.XssFilter',
  7.              'clean' => 'all'
  8.           )
  9.   );
  10. }

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

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

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

  1.   /**
  2.    *  @author  Opeykin A. &lt;andrey.opeykin.ru&; &lt;aopeykin@gmail.com&gt;
  3.    *  @version 0.0.1
  4.    *  @package filters
  5.    *
  6.    * Фильтр предназначен для фильтрации входных данных, c целью предотвратить xss атаки.
  7.    * Для фильтрации используются регулярные выражения из фреймворка Kohana 2.3.1
  8.    * @example
  9.    *
  10.    *  public function filters()
  11.    *  {
  12.    *         return array(
  13.    *                 array('application.filters.XssFilter',
  14.    *                       'clean' =>; 'all'
  15.    *                 )
  16.    *         );
  17.    *
  18.    *   }
  19.    *
  20.    *   В качетве параметра 'clean' могут быть:
  21.    *  - 'all' — фильтруются GET,POST,COOKIE,FILES массивы;
  22.    *  - '*'   — аналог ALL;
  23.    *  - так же возможно сочетание любых из параметров, например GET,COOKIE или POST,FILES
  24.    */
  25.  
  26. class XssFilter extends CFilter
  27. {
  28.    public  $clean = 'all';
  29.  
  30.   protected function preFilter($filterChain)
  31.   {
  32.   $this->clean  = trim(strtoupper($this->clean));
  33.   $data = array(
  34.   'GET'    =>&$_GET,
  35.   'POST'   =>&$_POST,
  36.   'COOKIE' =>&$_COOKIE,
  37.   'FILES'  =>&$_FILES
  38.   );
  39.  
  40.   if($this->clean === 'ALL' || $this->clean === '*')
  41.   {
  42.     $this->clean = 'GET,POST,COOKIE,FILES';
  43.   }
  44.   $dataForClean = split(',',$this->clean);
  45.   if(count($dataForClean))
  46.   {
  47.      foreach ($dataForClean as $key => $value)
  48.      {
  49.         if(isset ($data[$value]) && count($data[$value]))
  50.         {
  51.            $this->doXssClean($data[$value]);
  52.         }
  53.      }
  54.  }
  55. return true;
  56. }
  57.  
  58.   protected function postFilter($filterChain)
  59.   {
  60.     // logic being applied after the action is executed
  61.   }
  62.  
  63.   private function doXssClean(&$data)
  64.   {
  65.      if(is_array($data) && count($data))
  66.   {
  67.     foreach($data as $k => $v)
  68.   {
  69.      $data[$k] = $this->doXssClean($v);
  70.   }
  71.      return $data;
  72. }
  73.  
  74. if(trim($data) === '')
  75. {
  76. return $data;
  77. }
  78.  
  79. // xss_clean function from Kohana framework 2.3.1
  80. $data = str_replace(array('&amp;amp;','&amp;lt;','&amp;gt;'), array('&amp;amp;amp;','&amp;amp;lt;','&amp;amp;gt;'), $data);
  81. $data = preg_replace('/(&amp;#*\w+)[\x00-\x20]+;/u', '$1;', $data);
  82. $data = preg_replace('/(&amp;#x*[0-9A-F]+);*/iu', '$1;', $data);
  83. $data = html_entity_decode($data, ENT_COMPAT, 'UTF-8');
  84. // Remove any attribute starting with "on" or xmlns
  85. $data = preg_replace('#(&lt;[^&gt;]+?[\x00-\x20"\'])(?:on|xmlns)[^&gt;]*+&gt;#iu', '$1&gt;', $data);
  86. // Remove javascript: and vbscript: protocols
  87. $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);
  88. $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);
  89. $data = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*-moz-binding[\x00-\x20]*:#u', '$1=$2nomozbinding…', $data);
  90. // Only works in IE: &lt;span style="width: expression(alert('Ping!'));"&gt;&lt;/span&gt;
  91. $data = preg_replace('#(&lt;[^&gt;]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?expression[\x00-\x20]*\([^&gt;]*+&gt;#i', '$1&gt;', $data);
  92. $data = preg_replace('#(&lt;[^&gt;]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?behaviour[\x00-\x20]*\([^&gt;]*+&gt;#i', '$1&gt;', $data);
  93. $data = preg_replace('#(&lt;[^&gt;]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:*[^&gt;]*+&gt;#iu', '$1&gt;', $data);
  94. // Remove namespaced elements (we do not need them)
  95. $data = preg_replace('#&lt;/*\w+:\w[^&gt;]*+&gt;#i', '', $data);
  96. do
  97. {
  98. // Remove really unwanted tags
  99. $old_data = $data;
  100. $data = preg_replace('#&lt;/*(?:applet|b(?:ase|gsound|link)|embed|frame(?:set)?|i(?:frame|layer)|l(?:ayer|ink)|meta|object|s(?:cript|tyle)|title|xml)[^&gt;]*+&gt;#i', '', $data);
  101. }
  102. while ($old_data !== $data);
  103. return $data;
  104. }
  105.  
  106. }

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

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

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

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

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

Скачать xssfilter

Основной сайт Юпи! — http://yupe.ru

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

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