Суть таких инъекций – внедрение в данные (передаваемые через GET, POST запросы или значения Cookie) произвольного SQL кода. Если сайт уязвим и выполняет такие инъекции, то по сути есть возможность творить с БД (чаще всего это MySQL) что угодно.
Глупые огрехи в коде, позволяющие сломать страницу, вычислить довольно легко. Допустим, имеется тестовый сайт http://test.ru
. На сайте выводится список новостей, с возможностью детального просмотра. Адрес страницы с детальным описанием новости выглядит так: http://test.ru/?detail=1
. Т.е через GET запрос переменная detail передаёт значение 1 (которое является идентификатором записи в таблице новостей).
Изменяем GET запрос на ?detail=1'
или ?detail=1"
. Далее пробуем передавать эти запросы серверу, т.е заходим на http://test.ru/?detail=1'
или на http://test.ru/?detail=1"
.
Если при заходе на данные страницы появляется ошибка, значит сайт уязвим на SQL инъекции. Пример ошибки, возникающей при проверке уязвимости:
Итак, у нас есть уже упоминавшийся сайт http://test.ru
. В базе хранится 4 новости, 3 из которых выводятся. Разрешение на публикацию новости зависит от параметра public (если параметр содержит значение 1, то новость публикуется). Список новостей, разрешённых к публикации:
При обращении к странице http://test.ru/?detail=4
, которая должна выводить четвёртую новость появляется ошибка – новость не найдена. В нашем случае новость существует, но она запрещена к публикации.
Но так как мы уже проверяли сайт на уязвимость и он выдавал ошибку БД, то пробуем перебирать возможные варианты запросов. В адресной строке плюс +
выполняет роль пробела, так что не пугайтесь.
Тестирую следующие варианты:
http://test.ru/?detail=4+OR+1
http://test.ru/?detail=4+--
http://test.ru/?detail=4+UNION+SELECT+*+FROM+news+WHERE+id=4
В итоге удача улыбнулась и два запроса (первый и третий) вернули нам детальное описание четвёртой новости:
За получение детального описания новости отвечает блок кода:
$detail_id=$_GET['detail'];
$zapros="SELECT * FROM `$table_news` WHERE `public`='1' AND `id`=$detail_id ORDER BY `position` DESC";
Мало того, что $detail_id
получает значение без какой либо обработки, так ещё и конструкция id=$detail_id
написана криво, лучше придерживаться id='$detail_id'
(т.е. сравниваемое значение писать в прямых апострофах).
Глядя на запрос, получаемый при обращении к странице через http://test.ru/?detail=4+OR+1
:
SELECT * FROM `news` WHERE `public`='1' AND `id`=4 OR 1 ORDER BY `position` DESC
становится не совсем ясно, почему отобразилась 4-ая новость. Дело в том, что запрос вернул все записи из таблицы новостей, отсортированные в порядке убывания сверху. И таким образом наша 4-ая новость оказалась самой первой, она же и вывелась как детальная, просто совпадение.
Разбираем запрос, сформированный при обращении через http://test.ru/?detail=4+UNION+SELECT+*+FROM+news+WHERE+id=4
. Тут название таблицы с новостями (в нашем случае это news) бралось логическим перебором.
Итак, выполнился запрос:
SELECT * FROM `news` WHERE `public`='1' AND `id`=4 UNION SELECT * FROM news WHERE id=4 ORDER BY `position` DESC
К нулевому результату первой части запроса (до UNION) присоединился результат второй части (после UNION), вернувшей детальное описание 4-ой новости.
Защита от взлома сводится к базовому правилу «доверяй, но проверяй». Проверять нужно всё – числа, строки, даты, данные в специальных форматах.
Для проверки переменной на числовое значение используется функция is_numeric(n);
, которая вернёт true, если параметр n - число, и false в противном случае.
Так же можно не проверять значение на число, а вручную переопределить тип. Вот пример, переопределяющий значение $id
, полученное от $_GET['id_news']
в значение целочисленного типа (в целое число):
$id=(int)$_GET['id_news'];
Большинство взломов через SQL происходят по причине нахождения в строках «необезвреженных» кавычек, апострофов и других специальных символов. Для такого обезвреживания нужно использовать функцию addslashes($str);
, которая возвращает строку $str
с добавленным обратным слешем \
перед каждым специальным символом. Данный процесс называется экранизацией.
$a="пример текста с апострофом ' ";
echo addslashes($a); //будет выведено: пример текста с апострофом \'
Кроме этого существуют две функции, созданные именно для экранизации строк, используемых в SQL выражениях. Это функция mysql_escape_string($str);
и mysql_real_escape_string($str);
.
Первая не учитывает кодировку соединения с БД и может быть обойдена, а вот вторая её учитывает и абсолютно безопасна, mysql_real_escape_string($str);
возвращает строку $str
с добавленным обратным слешем к следующим символам: \x00, \n, \r, \, ', " и \x1a
.
Магические кавычки – функция автоматической замены кавычки на обратный слеш \
и кавычку при операциях ввода/вывода. В некоторых конфигурациях PHP этот параметр включён, а в некоторых нет. Для того, что бы избежать двойного экранизирования символов и заэкранизировать данные по-нормальному через mysql_real_escape_string($str);
, необходимо убрать автоматические проставленные обратные слеши (если магические кавычки включены).
Как-то раз кто-то сетовал, что параметр get_magic_quotes_gpc
ушёл в прошлое и сейчас на хостингах он везде отключен. Доверившись таким знаниям, в одном из проектов я решил не делать проверку магических кавычек. В результате меня ждал неприятный сюрприз и понимание, что верить нужно только себе. Поэтому, не доверяйтесь на то, что может быть в теории, а всё-равно делайте проверку.
Проверка включённости магических кавычек для данных получаемых из GET, POST или Куков организуется через функцию get_magic_quotes_gpc()
; (возвращает 1 – если магические кавычки включены, 0 – если отключены).
Если магические кавычки включены (т.е. обратные слеши добавляются) и такое встречается чаще, то их нужно убрать. Это делается через функцию stripslashes($str);
(возвращает строку $str
без обратных слешей у кавычек и прямых апострофов).
В заключении код с полной экранизацией строк для записи в БД:
if(get_magic_quotes_gpc()==1)
{
$element_title=stripslashes(trim($_POST["element_title"]));
$element_text=stripslashes(trim($_POST["element_text"]));
$element_date=stripslashes(trim($_POST["element_date"]));
}
else
{
$element_title=trim($_POST["element_title"]);
$element_text=trim($_POST["element_text"]);
$element_date=trim($_POST["element_date"]);
}
$element_title=mysql_real_escape_string($element_title);
$element_text=mysql_real_escape_string($element_text);
$element_date=mysql_real_escape_string($element_date);