» » Доверяй, но проверяй: защита от SQL-инъекций

Доверяй, но проверяй: защита от SQL-инъекций

Доверяй, но проверяй: защита от SQL-инъекций


Вне всяких сомнений, SQL-инъекции являются одним из самых распространенных способов взлома сайта.
Едва ли не первое, что пытается провернуть взломщик – тестирование популярных инъекций.
В этом небольшом посте мы вкратце рассмотрим историю вопроса, методы борьбы с инъекциями,
а также напишем небольшой PHP-класс обертку для PDOStatement для безопасного подключения и взаимодействия с MySQL-сервером (MySQL в данном случае приводится лишь по причине наибольшей распространенности, при желании все нижеследующее может быть адаптировано и на другие СУБД).

Суть вопроса:

Что, в сущности, из себя представляет инъекция? Начинающий php-разработчик, только приступивший к изучению MySQL, вероятнее всего, будет конструировать запросы примерно следующим образом:
$query = "SELECT name FROM mytable WHERE id=" . $_GET['id'];

В данном случае, PHP запрашивает у MySQL значение name соответствующее id, полученному через GET. Вероятно, id был введен из формы или возник в результате перехода пользователя по ссылке вида http://somedomain.com/?id=5. Сконструированный таким образом запрос обычно отправляется на сервер с использованием популярной php-библиотеки mysqli.

В случае добропорядочного пользователя, скрипт сработает именно так, как и задумывалось. Что же попробует сделать злоумышленник? Он не станет отправлять на сервер числовое значение. Вместо этого он, попробует поставить после числового значения точку с запятой (символ завершения SQL-запроса) после чего напишет свой, уже не имеющий к изначально задуманному нами запрос. Что-то вроде того:
5; SELECT * FROM mytable;

или еще страшнее:
5; DROP TABLE mytable;

В результате, MySQL-сервер сначала выполнит наш запрос, а следом за ним – запрос злоумышленника. Таким образом, взломщик, в зависимости от ситуации (и прав MySQL юзера, который используется PHP) сделать почти все что угодно: от получения доступа к конфиденциальной информации до полного контроля над вашей базой данных.

Как же разработчику защитить свой сайт от инъекций?

Ручная проверка пользовательских данных. Разумеется, проверка формата вводимых данных - первый наиболее очевидный вариант защиты от инъекций. Вместо того чтобы доверять вводимым данным мы будем проверять их на соответствие нужному нам формату. Делать это можно десятком различных способов. Например:
$query = "SELECT name FROM mytable WHERE id=" . intval($_GET['id']);

Или:
if( intval ( $_GET['id'] ) ) {
	$query = "SELECT name FROM mytable WHERE id=" . intval($_GET['id']);
} else {
	exit('айайай!');
}

В данном случае, полученная из GET-а информация перед отправкой проверяется на числовое значение. Любое значение, отличное от числового, введенное злоумышленником не будет отправлено на MySQL-сервер. Для валидации более сложных значений можно использовать регулярные выражения.
Другим вариантом примитивной защиты является использование нативной функции mysqli_real string_escape() для предотвращения проникновения «корварных» запросов. Главным минусом подобного подхода, является его крайняя неавтоматизированность: нам приходится проверять пользовательское значение каждый раз когда мы делаем запрос. Можно, конечно, использовать готовый SQL-билдер со встроенной защитой от SQL-инъекций или написать свой. Однако, в данном посте мы предлагаем несколько иной подход, основанный на использовании PDO.

Использование PDOStatement

Начиная с версии 5.1 в PHP доступен встроенный класс PDO (PHP Data Objects). Данный класс содержит богатый набор методов для работы с широким спектром баз данных. Несмотря на почтенный возраст данного инструмента, многие разработчики им пренебрегает, предпочитая «по-старинке» пользоваться библиотекой mysqli, в том числе и мы в DLE, но у нас есть ряд важный причин, DLE это старый скрипт, который появился задолго до PDO, и у нас есть обязательства по совместимости, как со старыми версиями скрипта, так и по максимальному упрощению процесса обновления, для тех, кто пользуется сторонними модулями. Плюс мы очень тщательно подходим к вопросам фильтрации входящих данных. Но вы в отличие от нас не такие старые «динозавры», поэтому главная мысль, которую мы хотим донести до вас заключается в следующем: прямое использования mysqli без каких-либо оберток – прямой путь к SQL-инъекциям, поэтому пишите код сразу безопасным. У разработчиков, по сути, есть только три выхода:

использовать ручную проверку пользовательских данных
написать собственную библиотеку (или взять готовую) на основе mysqli с защитой от SQL-инъекций
использовать PDO
Именно о последнем способе и пойдет разговор. В PDO имеется специальный набор методов, называемый PDOStatement. Его сущность заключается в том, что сам запрос и значения столбцов или параметров запроса передаются на MySQL-сервер отдельно. Таким образом, мы получаем встроенную защиту от инъекций на все случаи жизни (ну или почти на все). В силу того, что PDOStatement – довольно таки громоздкая штука (с точки зрения строчек кода), удобнее всего пользоваться ей через самописную обертку, написанием которой мы сейчас и займемся. Дабы сразу приучать к хорошему, результаты своей деятельности мы оформим в виде класса.

Наш класс будет содержать всего 4 метода:

метод для соединения с базой
метод для проверки наличия соединения
метод для отправки безопасного запроса через PDOStatement
метод для разрыва соединения с базой
Итак приступим. Для работы с базой нам понадобятся 6 свойств:
class myClass {

	private $host = "localhost";
	private $dbname = "dbname";
	private $user = "username";
	private $pass = "userpassword";
	private $charset = "utf8";
	
	private $pdo;
}

Первые 5 содержат хост, имя базы, логин, пароль и кодировку и не нуждаются в пояснении. В 6-е свойство же пригодится нам для хранения объекта pdo. Первым делом следует, конечно, написать метод для соединения:
public function mysql_connect() {$dsn = "mysql:host=" . $this->host . ";
dbname=" . $this->dbname . ";charset=" . $this->charset;
 $connopt = array(PDO::ATTR_ERRMODE  => 
 PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE => 
 PDO::FETCH_ASSOC);$this->pdo = new PDO($dsn, $this->user, $this->pass, 
 $connopt);}

Первая строчка содержит набор параметров для соединения с сервером. Массив $connopt задает различные режимы работы с PDO. Эти режимы могут быть использованы для тонкой отладки ошибок и специфических ситуаций. Мы не будем вдаваться в подробности используемых опций в данном руководстве. Все интересующиеся могут обратиться к более подробной документации по PHP. Здесь же отметим, что в последней строчке мы создаем объект для работы с PDO, передавая конструктору заданные параметры, и пишем этот объект в наше свойство $pdo.

Итак, с базой мы соединились. Напишем же метод для проверки наличия соединения. С этой целью мы будем использовать метод getAttribute(PDO::ATTR_CONNECTION_STATUS). В случае нормального соединения он возвращает строчку "hostname via TCP/IP", где hostname – имя нашего хоста.
public function mysql_get_status() {
		if(is_null($this->pdo)) {
			return false; 
		} elseif ($this->pdo->getAttribute
(PDO::ATTR_CONNECTION_STATUS) === $this->host . "
 via TCP/IP") {
			return true;
		} else {
			return false;
		}
	}

Если свойство pdo пусто – значит соединение не создано (или было разрушено). Следующим этапом мы проверяем значение возвращаемое getAttribute и сравниваем его с нормальным. Если оно отличается – выкидываем в false. Если все прошло гладко – возвращаем true. Если нет – false.

Теперь, собственно, самое интересное. Напишем метод для отправки безопасного запроса к базе.
Наш метод будет иметь три аргумента:

тело самого запроса
ассоциативный массив с набором значений
бинарный аргумент, определяющий требуется ли вернуть данные из базы. Для выполнение SELECT’ов данный аргумент будет равен true, а для INSERT’овUPDATE’ов – false.
Итак, наш метод:
public function mysql_query($query,
 $placeholders = null, $select = true) {
if($this->mysql_get_status()) {
$stmt = $this->pdo->prepare($query);
	if(!is_null($placeholders)) {
	$stmt->execute($placeholders);
	} else {
		$stmt->execute();
	}
	if ($select) {
		$arr = $stmt->fetchAll();
		return $arr;
		}
	} else {
			return false; 
	}
	}

Давайте разберемся, что же здесь происходит. Первое условие необходимо для того, чтобы пытаться выполнять запрос только при наличии соединения с базой данных. Во втором условии, мы проверяем наличие массива значений (плейсхолдеров). Если массив не указан – значит мы выполняем запрос «как есть». Если же массив плейсхолдеров имеет место быть, мы передаем его методу execute() в качестве аргумента. Методы prepare() и execute() – и есть то ради чего все создавалось. Как вы могли заметить, при работе с базой через PDO тело запроса и его значения передаются PDO отдельно друг от друга. При этом сам запрос пишется в следующем виде:
"SELECT name FROM mytable WHERE id = :user_id"

Где :user_id – название ключа в массиве, передаваемом в execute(). То есть в явном виде отправка запроса с использованием PDOStatement выглядит примерно так:
$smtp = $pdo->prepare("SELECT name FROM mytable WHERE id = :user_id");
$stmt-execute( array ('user_id' => '5' ) );

Для разрушения соединения достаточно лишь присвоить null объекту pdo. Поэтому, метод для ликвидации соединения будет самым коротким:
public function mysql_destroy() {
$this->pdo = null;
}

Итак, соберем наш класс воедино:
class myClass {
	private $host = "localhost";
	private $dbname = "dbname";
	private $user = "username";
	private $pass = "userpassword";
	private $charset = "utf8";
	
	private $pdo;

	public function mysql_connect() {
		$dsn = "mysql:host=" . $this->host . ";dbname=" . $this->
dbname . ";charset=" . $this->charset;
		$connopt = array(
    		PDO::ATTR_ERRMODE  => PDO::ERRMODE_EXCEPTION,
    		PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
		);
		$this->pdo = new PDO($dsn, $this->user, $this->pass, $connopt);
	}
	public function mysql_get_status() {
		if(is_null($this->pdo)) {
			return false; 
		} elseif ($this->pdo->getAttribute(PDO::ATTR_CONNECTION_STATUS)
              === $this -> host . " via TCP/IP") {
			return true;
		} else {
			return false;
		}
	}
	public function mysql_query($query, $placeholders = null, 
           $select = true) {
		if($this->mysql_get_status()) {
			$stmt = $this->pdo->prepare($query);
	if(!is_null($placeholders)) {
		$stmt->execute($placeholders);
		} else {
	$stmt->execute();
	}
		if ($select) {
	$arr = $stmt->fetchAll();
		return $arr;
		}
		} else {
			return false; 
		}
	}

	public function mysql_destroy() {
		$this->pdo = null;
	}
}

Пример отправки безопасного запроса:

//устанавливаем соединение
$obj = new myClass; 
$obj->mysql_connect(); 

//SELECT
$data = $obj->mysql_query("SELECT name FROM mytable
 WHERE id = :user_id AND email=:email;", array('user_id' => '5', 'email' =>
 'somemail@mail.com' ), TRUE); 
print_r( $data ); 

//INSERT 
$obj->mysql_query("INSERT INTO mytable (‘name’, ‘email’) VALUES
 (:name, :email);", array( 'name' => 'Иван', 'email' => 'somemail@mail.com' ), 
FALSE); 

//разрываем соединение
$obj->mysql_destroy();


Источник: dle-news.ru

5-11-2016, 03:40 724 0

Комментарии


Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.