Yii, пишем фильтр для предотвращения XSS атак.
Начну с небольшого отступления.
И все таки правильно говорят, а на некторых форумах (особенно UNIX-овых), прямо кричат — RTFM! Кто не понял очем идет речь — RTFM в переводе означает «читай эту чертову документацию!». Это все я собственно вот к чему: изучая и что-то пытаясь написать на фреймворке Yii, возникла задача фильтрации входных данных от различного рода «зловредных» символов (аля XSS-атака) и первое что пришло в голову — это написать свой фильтр (что я все таки и сделал), однако creocoder, на форуме Yii , совершенно спрпаведливо заметил, что не зачем изобретать велосипед, все уже есть готовое, необходимо только RTFM! Речь шла о классе CHtmlPurifier, который является оберткой для библиотеки HTML Purifier , и выполняет все те функции, которые мне необходимы (правда я так и не попробывал его в действии, может и зря конечно). Но раз уж я начал писать свой фильтр — решил все таки это дело завершить, да и просто написать статью о фильтрах в Yii.
И так!
Фильтры — фрагменты кода, которые могут быть выполнены до и\или после выполнения экшена контроллера. Фильтры, при необходимости, могут не допустить выполнения запрошенного экшена.
Фильтры могут быть как методами текущего контроллера, так и отдельными классами — что позволяет повторно их использовать. Если фильтр реализуется как метод класса, он должен иметь префикс «filter».
Пример:
-
public function filterAccessControl()
-
{
-
…….
-
}
Фильтр, реализованный в виде отдельного класса, должен быть наследником класса CFilter.
Пример:
-
class XssFilter extends CFilter
-
{
-
// код который выполнится <strong>до </strong>выполнения экшена
-
public function preFilter()
-
{
-
…….
-
}
-
// код который выполнится<strong> после</strong> выполнения экшена
-
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 для этих целей, но «родная» возможность сделать это была бы лучшим вариантом.
Надеюсь, описанный материал окажется полезен!
Основной сайт Юпи! — http://yupe.ru
Исходный код — https://github.com/yupe/yupe
Присоединяйтесь!
