Суть вопроса:
Что, в сущности, из себя представляет инъекция? Начинающий 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
Наш класс будет содержать всего 4 метода:
- метод для соединения с базой
- метод для проверки наличия соединения
- метод для отправки безопасного запроса через PDOStatement
- метод для разрыва соединения с базой
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();
На этом пока все. Удачи вам и хорошего настроения. Подписывайтесь на нашу страницу в социальной сети "Вконтаке" https://vk.com/dlepage
Комментарии