Yii, пишем фильтр для предотвращения XSS-атак
Начну с небольшого отступления.
И все таки правильно говорят, а на некторых форумах (особенно UNIX-овых), прямо кричат — RTFM! Кто не понял очем идет речь — RTFM в переводе означает «читай эту чертову документацию!». Это все я собственно вот к чему: изучая и что-то пытаясь написать на фреймворке Yii, возникла задача фильтрации входных данных от различного рода «зловредных» символов (аля XSS-атака) и первое что пришло в голову — это написать свой фильтр (что я все таки и сделал), однако creocoder, на форуме Yii, совершенно спрпаведливо заметил, что не зачем изобретать велосипед, все уже есть готовое, необходимо только RTFM! Речь шла о классе CHtmlPurifier, который является оберткой для библиотеки HTML Purifier, и выполняет все те функции, которые мне необходимы (правда я так и не попробывал его в действии, может и зря конечно). Но раз уж я начал писать свой фильтр — решил все таки это дело завершить, да и просто написать статью о фильтрах в Yii.
И так!
Фильтры — фрагменты кода, которые могут быть выполнены до и\или после выполнения экшена контроллера. Фильтры, при необходимости, могут не допустить выполнения запрошенного экшена.
Фильтры могут быть как методами текущего контроллера, так и отдельными классами — что позволяет повторно их использовать. Если фильтр реализуется как метод класса, он должен иметь префикс «filter».
Пример:
public function filterAccessControl() { ....... }</code>
Фильтр, реализованный в виде отдельного класса, должен быть наследником класса 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('&','<','>'), $data); $data = preg_replace('/(*\w+)[\x00-\x20]+;/u', '$1;', $data); $data = preg_replace('/(*[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 для этих целей, но «родная» возможность сделать это была бы лучшим вариантом.
Надеюсь, описанный материал окажется полезен!
Комментарии