Скачать исходник к статье
Intro
В наши дни ни один сайт не обходится без скриптов. В большинстве своем все скрипты написаны на PHP, и зачастую пишут их не профессионалы, поэтому в таких скриптах вероятность нахождения уязвимости достаточно высока. В подавляющем большинстве скриптов уязвимость сводится к php/sql injection, либо XSS. Причины таких багов почти всегда кроются в неправильной или недостаточно тщательной обработке входных данных. В этой статье я расскажу как можно контролировать входные данные, не изменяя самого кода скрипта.
WebFirewall->conceptИдея Web Firewall в чем-то схожа с IDS - Intrusion Detection System. Ее суть заключается в том, что между клиентом и сервером находится некий посредник, который обрабатывает и анализирует весь входящий на сервер трафик. В случае нахождения в трафике недопустимых или подозрительных данных IDS предпринимает определенные действия по ликвидации возможной опасности. В IDS как правило используются различные интеллектуальные алгоритмы распознавания атак, но в нашей системе будет в основном простая фильтрация входящих данных, поэтому правильней будет назвать ее фаерволом. Алгоритм работы Web Firewall (WF) будет следющим:
1) WF выполняется в скрипте до запуска основного функционального кода.
2) WF анализирует все переданные скрипту переменные, с помощью заранее установленных ограничений на их содержимое.
3) Если содержимое какой-то переменной не будет соответствовать определенному для этой переменной правилу, то WF будет выполнять действия по защите скрипта (например, экстренное завершение скрипта и запись в лог о попытке проникновения).
При использовании такого фаервола нам не нужно будет исправлять код всего скрипта для исключения уязвимости. Достаточно знать имена всех входящих переменных и их возможное содержимое, после чего просто подключить фаервол в начале скрипта.
WebFirewall->coding->conceptДля написания фаервола мы будем использовать PHP 5. Сам фаервол будет представлять собой класс с соответствующими функциями. Плюсом при использовании классов является более гибкая настройка и использование WF, например, можно создать сразу несколько WF и распараллелить задачи защиты между ними.
Все входящие данные разделим на 3 группы (по методу загрузки):
1) GET-данные, которые хранятся в массиве $_GET
2) POST-данные, хранятся в массиве $_POST
3) COOKIE-данные, хранятся в $_COOKIE
Теперь рассмотрим возможные виды нарушения безопасности. Можно составить такой список:
1) Переменная пришла не тем методом. Например, если предполагается что переменная $foo должна приходить только из COOKIE, а она пришла в POST или GET данных, то такая ситуация будет считаться нарушением безопасности.
2) Переменная содержит недопустимые данные. Например, если в переменной, которая должна принимать целочисленные значения нам пытаются подсунуть что-то типа "?1? OR 1=1? /*", то WF должен пресечь это.
3) Переменная содержит слишком большие данные. Аналогично пункту 2, WF должен пресекать работу с переменными, длина которых превышает допустимый предел.
4) Пришла непонятно какая переменная, которой вобще не должно быть. Этот вид используется в основном с включенными REQGISTER_GLOBALS, когда через запрос можно менять внутренние переменные скрипта, либо когда скрипту присылают допустимые переменные, но нужные не в данный момент, а далее по ходу работы скрипта.
Итак, все эти 4 пункта могут быть потенциальными опаностями для любых из трех групп данных - GET, POST и COOKIE.
Действия WF в случае обнаружения нарушения защиты будут следующими: фаервол будет вызывать заранее определенную для каждого вида нарушения функцию, параметром в которую будет передаваться имя переменной, правило которой было нарушено.
WebFirewall->coding->codeИтак, создадим класс WebFirewall. Из переменных, доступных в любом месте скрипта, у нас будут два массива - $vars и $ignored.
В $ignored будем добавлять те переменные, которые вобще не будут обрабатываться фаерволом. Нужно это вот для чего: так как у нас под контролем должны быть абсолютно все входящие переменные, то мы не можем просто не добавить какую-то переменную в список фильтруемых, иначе она будет выловлена как "лишняя" (вариант 4 видов нарушения безопасности).
Каждый элемент массива $vars будет описывать одну переменную. Выглядеть он будет так :
$this->vars[?varname?] => array( array(?type1?, ?type2?, ...), $maxlength )
Здесь varname - имя переменной, type1, type2 - типы данных, которые может содержать эта переменная, $maxlength - максимальная длина переменной. У нас будет 4 типа содержимого переменных:
?num? - переменная является числом(состоит только из цифр);
?alnum? - переменная состоит из цифр и букв;
?char? - длина переменной равна 1;
?print? - переменная состоит из печатаемых символов;
Наконец, для расширения функциональности, если первый символ типа переменных будет ?~?, например ?~type~?, то скрипт будет сравнивать содержимое этой переменной со строкой ?~type~? как регулярное выражение. Вообще все вышеперечисленные типы содержимого можно записать в виде регулярного выражения, но так как их использование достаточно ресурсоемко, некоторые типы лучше записать простыми функциями.
Теперь добавим флаги переменной, с помощью которых фаервол сможет контролировать, каким методом пришла эта переменная:
?in_get? - переменная может приходить только GET методом
?in_post? - переменная может приходить только POST методом
?in_cookie? - переменная может приходить только через COOKIE
Например, следующая строка:
$this->vars[?id?] => array( array(?num?, ?~6~?, ?in_get?), 5 )
означает, что переменная id должна иметь длину не более 5 символов, состоять только из цифр и содержать цифру 6. Кроме того эта переменная может приходить только через GET. Пример правильного применения такого правила: http://somehost.com/index.php?id=363
Теперь перейдем к конструктору класса. При создании, классу передаются 4 имени функции, которые обрабатывают 4 вида нарушений.
class WebFirewall { public $vars = array(), // список переменных $ignore = array(); // список игнорируемых переменных // обработчики нарушения безопасности private $hnd_nonex, // хендлер переменной, которой нет ни в $vars ни в $ignored $hnd_overflow, // хендлер слишком длинной переменной $hnd_type, // хендлер переменной с неправильным содержимым $hnd_method; // хендлер переменной, пришедшей не тем методом, который // требуется. public function __construct($h_nonex = false, $h_type = false, $h_overflow = false, $h_method = false) { // устанавливаем обработчики переменных $this->hnd_nonex = $h_nonex; $this->hnd_overflow = $h_overflow; $this->hnd_type = $h_type; $this->hnd_method = $h_method; } /** Здесь остальные функции */ }
Если в конструктор вместо обработчика нарушения передать false, то при вызове метода проверки в фаерволе просто будет возвращать false.
Теперь напишем процедуру анализа переменной. В ней мы просматриваем все простейшие типы в правиле для переменной и сравниваем эту переменную с каждым типом. Затем проверяем максимальную длину переменной, если нужно. Эта процедура универсальна для GET, POST и COOKIE методов, поэтому проверять переменную на корректный метод мы будем не в этой процедуре, а параллельно ей - в другой.
private function VarCheck($name, &$value) { foreach ($this->vars[$name][0] as $type) { switch ($type) { case ?num?: if (!$this->is_num($value)) { $this->PanicVarBadType($name); // недопустимый тип return false; } break; /* case .... - здесь сравниваем с другими типами */ // если тип начинается с ~, то сравниваем как регулярное выражение. default: if ($type[0] == ?~? && !preg_match($type,$value)) { $this->PanicVarBadType($name); return false; } } } if (($this->vars[$name][1] != 0) && strlen($value) > $this->vars[$name][1]){ $this->PanicVarMaxLength($name); // слишком большая длина переменной return false; } return true; }
Здесь функции $this->PanicVarBadType() и $this->PanicVarBadPlace() вызывают обработчики соответствующих нарушений:
// вызов хендлера слишком длинной переменной private function PanicVarMaxLength($varname) { if ( $this->hnd_overflow ) { // если хэндлер установлен, вызываем функцию call_user_func($this->hnd_overflow, $varname); } }
Функции $this->PanicVarBadType() и $this->PanicVarBadPlace() аналогичны.
Собственно функция запуска работы всего WF будет состоять из последовательного вызова проверок GET, POST, COOKIE переменных.
public function CheckVars() { $this->CheckGetVars(); // проверяем все GET переменные $this->CheckPostVars(); // проверяем все POST переменные $this->CheckCookieVars(); // проверяем все COOKIE переменные }
Каждая из этих трех функций проверяет переменные, пришедшие соответствующим методом, сначала с помощью функции $obj->VarCheck(), а затем проверяет, не пришла ли эта переменная другими методами, если указан соответствующий флаг (?in_get?, ?in_post? или ?in_cookie?):
public function CheckGetVars() { foreach ($_GET as $k=>$v) { // перебираем все GET переменные // если переменная в списке игнорирования, пропускаем ее if (in_array($k, $this->ignore)) { break; } // если переменной нет в списке правил, она лишняя if (!array_key_exists($k, $this->vars)) { $this->PanicVarNotExist($k); break; } // переменная также пришла if (in_array( ?in_post?, $this->vars[$k][0]) || in_array( ?in_cookie?, $this->vars[$k][0])) { $this->PanicVarBadPlace($k); } $this->VarCheck($k, $v); } return true; }
Итак, общий алгоритм работы Web Firewall таков:
1) Составляем правила для переменных;
2) Пишем обработчики нарушений;
3) Создаем объект класса WebFirewall;
4) Устанвливаем в этом объекте созданные ранее правила и обработчики;
5) Запускаем проверку переменных.
Посмотрим на работу фаервола в действии. Для начала опишем массив правил для входящих переменных:
$firewall_rules = array( ?news? => array( array(?alnum?) , 50), ?comment? => array( array(?~.*~?, ?in_post?), 1000), );
Здесь переменная news должна содержать только буквы и цифры и не может быть длиннее 50 символов. Переменная comment может содержать любые символы, не должна быть длиннее 1000 символов и может передаваться только методом POST.
Напишем функции-обработчики нарушений. Для простоты они будут просто завершать работу скрипта с выдачей соответсвующего сообщения:
// обработчик лишней переменной function Firewall_nonex($varname) { exit(?<font size="+1" color="red">Hacking attempt - ?.$varname.?<br />excess var</font>?); }
Остальные 3 функции аналогичны - Firewall_type(), Firewall_overflow(), Firewall_place().
Теперь создадим и запустим наш фаервол:
// создаем класс $firewall = new WebFirewall(?Firewall_nonex?, ?Firewall_type?, ?Firewall_overflow?, ?Firewall_place?); // устанавливаем правила $firewall->vars = $firewall_rules; // запускаем проверку $firewall->CheckVars(); echo ?all ok?;
Поскольку наши функции при несоблюдении правила завершают скрипт, то сообщение ?all ok? появится только в случае правильности всех данных.
Вот примеры запросов, которые не пройдут проверку:
index.php?news=1234@ // содержит символ @ ( не буква и не цифра)
index.php?comment=my_comment // данные переданы GET методом
index.php?foo=3 // foo - лишняя переменная, которой нет в правилах
Примеры запросов, в которых все в порядке:
index.php?news=123
index.php?news=zixter
Итак, используя этот класс, мы можем в любом скрипте подключить файл с этим классом и описанными правилами и обработчиками, отфильтровать все неправильные данные и возможно записать факт обнаружения попытки взлома в логи. Как уже было сказано, в сам скрипт не надо вносить никаких изменений (за исключением добавления строчки include "wf.php"), поэтому такую систему возможно эффективно и гибко использовать в движках малого и среднего размера, или в собственных скриптах для повышения общей безопасности.
Zixter [ http://pcr.cc ], 31.12.07
© Zixter 01.02.2008


