Когда я начинал программировать на PHP, еще мало кто думал о фильтрации переменных, все писали код на коленке и никто не слышал о словосочетании «SQL-инъекция». Не был исключением и я. Как и большинство людей, начинающих изучать веб-программирование, я не задумывался о том, какие переменные я получаю извне и где их использую. Я просто писал код, выполняющий именно то, что от него требовалось, а о безопасности я не беспокоился.
С тех пор утекло много воды, люди стали умнее, а способы взлома страниц — изощреннее. Сейчас, если ты не будешь отслеживать корректность данных, то тебя сможет взломать любой школьник, используя программы вроде Sqlmap или Havij (или любые другие). SQL injection, XSS, PHP injection — это лишь часть проблем, вызываемых непроверяемыми данными.
Помню, как однажды я сам начитался разных умных статей про SQLi и взломал за полдня собственный сайт. После этого я уже задумался, как уберечься от данной напасти.
Предупреждаю сразу — я не специалист по информационной безопасности. Я хакер, не гуру и даже не профессионал. Веб-программирование в большей части для меня — хобби, я не зарабатываю им на жизнь и не уделяю достаточно внимания развитию новых технологий. Так что я открыт для любых комментариев и советов и любой критике.
Подход у меня следующий:
- На этапе разработки сайта я определяю список переменных, которые мне нужно получать извне.
- Перед обработкой страницы я перебираю все известные переменные и осуществляю валидацию каждой из них.
- Все переменные помещаются в единый массив VARS.
- Когда необходимо обратиться к входной переменной — я обращаюсь именно к VARS['имя_переменной']
Теперь поподробнее.
Список переменных:
class Variables{
private static $Variables=array(
//...
array('name' => 'email', 'array' => '_POST', 'type' => 'Email'),
array('name' => 'firstname', 'array' => '_POST', 'type' => 'Name'),
//...
array('name' => 'id', 'array' => '_COOKIE', 'type' => 'Hash'),
array('name' => 'login', 'array' => '_POST', 'type' => 'UserName'),
array('name' => 'middlename', 'array' => '_POST', 'type' => 'Name'),
array('name' => 'name', 'array' => '_POST', 'type' => 'Text'),
//...
array('name' => 'page', 'array' => '_GET', 'type' => 'PageName'),
array('name' => 'password', 'array' => '_POST', 'type' => 'Plain'),
//...
array('name' => 'file', 'array' => '_FILES', 'type' => 'Plain'),
array('name' => 'scores', 'array' => '_POST', 'type' => 'Array', 'adv' => 'Num'),
//...
array('name' => 'title', 'array' => '_POST', 'type' => 'Text'),
//...
array('name' => 'user', 'array' => '_POST', 'type' => 'UserName')
);
//...
}
Зачем я для отдельного массива создаю целый статический класс, спросите вы? Для того, чтобы иметь доступ к нему везде. Иногда использовать такой способ организации статичного массива удобнее всего. Возможно стоит переписать этот класс, возможно используя ArrayAccess, но у меня руки никак до этого не доходят.
Итак, для каждой переменной мы храним имя, массив хранения и некий тип. Валидация переменной происходит именно на основании типа.
Далее, при загрузке страницы, в конструкторе контроллера происходит вызов
function __construct($page){
//...
$this->Validate();
//...
}
Функция валидации:
function Validate(){
$count=Variables::сount();
for($i=0;$i<$count;$i++){
$Var=Variables::getVariable($i); //$Var: array('name' => ..., 'array' => ..., 'type' => ...)
$Array=$GLOBALS[$Var["array"]]; //$Array: $_POST | $_GET | $_COOKIE | $_FILES
$Type=$Var["type"]; //
if(isset($Var['adv']))
$Adv=$Var['adv'];
else
$Adv=NULL;
if(isset($Array[$Var["name"]])){
$Value=$Array[$Var["name"]];
Validator::Validate($Value,$Type,$Adv); //Валидация
}
else
$Value=NULL;
if($Value!==NULL || !isset($this->VARS[$Var["name"]]))
$this->VARS[$Var["name"]]=$Value;
}
}
Мы перебираем все переменные из Variables и производим валидацию каждой.
Класс Validator:
class Validator{
function Validate(&$var,$type,$adv_param=NULL){
switch($type){
case 'PageName':
if(!self::IsPageName($var))$var=NULL; //Здесь и далее: Если переменная не прошла валидацию, то сбрасываем ее значение в null
break;
case 'UserName':
if(!self::IsUserName($var))$var=NULL;
break;
case 'Num':
if(!self::IsNum($var))$var=NULL;
break;
case 'Plain':
break;
case 'Text':
$var=htmlspecialchars($var,ENT_QUOTES,'cp1251',false);
break;
case 'Email':
if(!self::IsEMail($var))$var=NULL;
break;
case 'Array':
foreach($var as $key=>$value){
if(!self::IsNum($key)) //Поддерживаются только числовые ключи массива
unset($var[$key]);
else
self::Validate($var[$key],$adv_param); //Проверяем элемент массива
}
break;
case 'DateTime':
if(!self::IsDateTime($var))$var=NULL;
break;
case 'Name':
if(!self::IsName($var))$var=NULL;
break;
default:
$var='';
Throw new Exception("Invalid type for validation");
}
}
function IsNum($str){
return preg_match('{^[0-9-]+$}',$str);
}
function IsPageName($str){
return preg_match('{^[a-zA-Z0-9_]+$}', $str);
}
function IsUserName($str){
return preg_match('{^[A-Za-z0-9_]{2,32}$}', $str);
}
function IsHash($str){
return preg_match('{^[a-z0-9]{32,32}$}', $str);
}
function IsId($str){
return preg_match('{^[0-9]{1,6}$}', $str);
}
function IsRusName($str){
return preg_match('{^[а-яА-Я- ]+$}',$str);
}
function IsDateTime($str){
return preg_match('{^[0-9]{4}/[0-9]{2}/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$}',$str);
}
function IsName($str){
return preg_match('{^[A-Za-zа-яА-Я- ]+$}',$str);
}
}
В зависимости от типа переменной операции валидации могут быть различные. Большинство переменных проверяются на reqexp и несоответствующие значения фильтруются (пока просто обнуляются). Для типа Array проверяются элементы массивов. Текстовые переменные могут прогоняться через функции подобных htmlspecialchars. Пароли и файлы отстаются «как есть».
На выходе всего этого мы имеем массив $this->VARS, заполненный отфильтрованными переменными. С переменные из VARS мы можем делать безбоязно все, что хотим. Можем не глядя внести в БД, можем вывести на страницы (за исключением переменных, у которых в этом смысла нет — пароли и файлы).
По моему мнению, с такой схемой фильтрации можно спасть спокойно и не беспокоится об инъекциях…
Автор: vkar