Как я фильтрую переменные в PHP

в 21:59, , рубрики: php, sql-инъекция, информационная безопасность, Песочница, метки: ,

Когда я начинал программировать на 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js