webew
Войти » Регистрация
 
MySQL
PHP :: безопасность

SQL-инъекции

5 февраля 2009, 22:11
Автор: Григорий Рубцов [rgbeast]

SQL-инъекции — встраивание вредоносного кода в запросы к базе данных — наиболее опасный вид атак. С использованием SQL-инъекций злоумышленник может не только получить закрытую информацию из базы данных, но и, при определенных условиях, внести туда изменения.

Уязвимость по отношению к SQL-инъекциям возникает из-за того, что пользовательская информация попадает в запрос к базе данных без должной обработке: чтобы скрипт не был уязвим, требуется убедиться, что все пользовательские данные попадают во все запросы к базе данных в экранированном виде. Требование всеобщности и является краеугольным камнем: допущенное в одном скрипте нарушение делает уязвимой всю систему.

Пример уязвимости

Предположим, имеется скрипт, отображающий список пользователей из данного города, принимающий в качестве GET-параметра id города. Обращение к скрипту будет происходить с помощью HTTP по адресу /users.php?cityid=20

<?php
// подключение к базе данных
$sql = "SELECT username, realname FROM users WHERE cityid=" . $_GET['cityid'];
$result = mysql_query($sql) or die(mysql_error());
// обработка результата и отображение списка пользователей
?>

В скрипте выше разработчик вставляет GET-параметр в SQL-запрос, подразумевая, что в GET-параметре всегда будет число. Злоумышленник может передать в качестве параметра строку и тем самым повредить запрос. Например, он обратится к скрипту как /users.php?cityid=20; DELETE * FROM users
SQL-запрос получится таким:

SELECT username, realname FROM users WHERE cityid=20; DELETE * FROM users

Получается, что сервер MySQL получит не один запрос, а уже два, второй из которых нежелателен. К счастью, от этого существует защита: не допускается передавать два запроса одним mysql_query(). Поэтому злоумышленник должен встраивать свой кусок хитрее. Например, так: /users.php?cityid=20 UNION SELECT username, password AS realname FROM users
Запрос к БД будет иметь вид:

SELECT username, realname FROM users WHERE cityid=20 UNION SELECT username, password AS realname FROM users

Запрос выполнится, и скрипт выдаст не только пользователей из заданного города, но и список всех пользователей, у которых вместо реального имени отобразится пароль.

Как защититься?

Давайте заключим пользователькую информацию в одинарные кавычки. Поможет ли это?

$sql = "SELECT username, realname FROM users WHERE cityid='" . $_GET['cityid'] . "'";

Оказывается, что такая мера не помогает. Злоумышленник сможет передать параметр cityid, содержащий одинарные кавычки, нейтрализующие эффект от защитных кавычек. В качестве параметра cityid он передаст 20' UNION SELECT username, password AS realname FROM users WHERE '1, что приведет с формированию следующего запроса:

SELECT username, realname FROM users WHERE cityid='20' UNION SELECT username, password AS realname FROM users WHERE '1'

Из примера выше видно, что заключить в одиночные кавычки недостаточно. Необходимо также экранировать все кавычки, содержащиеся в строке. Для этого в PHP предусмотрена функция mysql_real_escape_string(), которая добавляет обратный слеш перед каждой кавычкой, обратной кавычкой и некоторыми другим спецсимволами. Рассмотрим код:

$sql = "SELECT username, realname FROM users WHERE cityid='" . mysql_real_escape_string($_GET['cityid']) . "'";

В случае использования mysql_real_escape_string() действия злоумышленника приведут к формированию запроса, который не является опасным, так как весь текст теперь внутри кавычек:

SELECT username, realname FROM users WHERE cityid='20\' UNION SELECT username, password AS realname FROM users WHERE \'1'

Итак, чтобы защититься от SQL-инъекций, все внешние параметры, которые могут содержать текст, должны быть перед включением в SQL-запрос обработаны с помощью mysql_real_escape_string() и заключены в одиночные кавычки.

Если известно, что параметр должен принимать числовое значение числовым, его можно привести к числовому виду явно с помощью функции intval() или floatval(). В данном примере мы могли бы использовать:

$sql = "SELECT username, realname FROM users WHERE cityid='" . intval($_GET['cityid']) . "'";

Отличия mysql_real_escape_string() и mysql_escape_string()

mysql_real_escape_string() является усовершенствованной версией функции mysql_escape_string(), широко применяемой для формирования безопасных запросов к БД MySQL. Отличия этих двух функций в том, что mysql_real_escape_string() правильно работает с многобайтовыми кодировками.

Предположим, в обрабатываемых данных есть символ (скажем, в UTF-8), код которого состоит из двух байт — шестнадцатеричных 27 и 2B (десятичные 39 и 43 соответственно). mysql_escape_string() воспринимает каждый байт передаваемых ей данных как отдельный символ (точнее, как код отдельного символа) и решит, что последовательность байт 27 и 2B — это два разных символа: одинарная кавычка (') и плюс (+). Поскольку функция воспринимает кавычку как специальный символ, перед байтом с кодом 27, который на самом деле является частью какого-то безобидного символа, будет добавлен слэш (\). В результате данные отправятся в базу в искаженном виде.

Стоит отметить, что mysql_real_escape_string() работает правильно во всех случаях и может полностью заменить mysql_escape_string().

mysql_real_escape_string() доступна в PHP с версии 4.3.0.

Дополнительные примеры

Мы рассмотрели наиболее простой пример, но на практике уязвимый запрос может быть более сложным и не отображать свои результаты пользователю. Далее рассмотрим примеры SQL-инъекций в некоторых более сложных случаях, не претендуя на полноту.

Инъекция в сложных запросах

В простейшем примере была возможность встроить код в конец SQL-запроса. На практике в конце SQL-запроса могут быть дополнительные условия, операторы сортировки, группировки и другие SQL-конструкции. В каждом конкретном случае, злоумышленник постарается встроить вредоносный кусок таким образом, чтобы запрос в целом остался синтаксически корректным, но выболнял другую функцию. Здесь мы рассмотрим простейший пример уязвимого запроса с дополнительным условием.

$sql = "SELECT username, realname FROM users WHERE cityid='" . $_GET['cityid'] . "' AND age<'35'";

В этом случае, злоумышленник может нейтрализовать дополнительное условия, передав в качестве параметра cityid 20' UNION SELECT username, password AS realname FROM users WHERE 1 OR '1, что приведет с формированию следующего запроса:

SELECT username, realname FROM users WHERE cityid='20' UNION SELECT username, password AS realname FROM users WHERE 1 OR '1' AND age<'35'

В результате условие age<35 не будет влиять на выборку, т.к. оператор OR имеет более низкий приоритет, чем AND, и WHERE из приведённого выше запроса по-другому можно записать в виде WHERE (cityid='20' AND 1) OR ('1' AND
age<'35')
(напомним, что выражение WHERE 1 истинно всегда). В результате под условие подойдут и те строки, у которых cityid='20', и те, у которых age<35, причем наличие последних не обязательно.

В случае сложных запросов успешные SQL-иъекции требуют некоторой изобретательности, но можно ожидать, что у злоумышленников она имеется.

Результаты запроса не отображаются пользователю

Может оказаться, что уязвимым является запрос, результаты которого не отображаются пользователю. Это может быть, например, вспомогательный запрос:

$sql = "SELECT count(*) FROM users WHERE userid='" . $_GET['userid'] . "'";

Запрос выше всего лишь проверяет наличие пользователя с данным userid: если он возвращает любую отличную от нуля величину — показывается профиль пользователя с соответствующим userid, если же возвращён 0 (то есть, нет пользователей, удовлетворяющих критерию запроса) — сообщение "пользователь не найден".

В этом случае определение пароля (или другой информации) производится перебором. Взломщик передает в качестве параметра userid строку 2' AND password LIKE 'a%. Итоговый запрос: SELECT count(*) FROM users WHERE userid='2' AND password LIKE 'a%'

Взломщик получит "пользователь не найден", если пароль не начинается на букву 'a', или стандартную страницу с профилем пользователя, в противном случае. Перебором определяется первая буква пароля, затем вторая и.т.д.

.

Выводы

Удачи в построении безопасных приложений!

Статья написана по материалам онлайн-курса «Программирование на PHP».


© Все права на данную статью принадлежат порталу webew.ru. Перепечатка в интернет-изданиях разрешается только с указанием автора и прямой ссылки на оригинальную статью. Перепечатка в печатных изданиях допускается только с разрешения редакции.
Добавить комментарий
Отображение комментариев: Древовидное | Плоское

bur

Спасибо за статью!

Раз комбинация одиночные кавычки + mysql_real_escape_string является универсальной защитой, из этого можно сделать обертку, дублируя хэши $_GET, $_POST и $_COOKIE в обработанные аналоги:
foreach ($_GET as $name => $value)
    $_GET_PARSED[$name] = "'" . mysql_real_escape_string($value) . "'";

, а потом использовать только PARSED хеши. Главное не забыть о такой обертке, чтобы не получилось как с магическими кавычками :-)

Кстати, а как в mysql с ошибками переполнения буфера?
Fastcoder.org — портал для JavaScrpt-программистов
16.02.2009, 14:39
Ответить
NO USERPIC

rgbeast

Сложность в том, что GET-параметры обычно сначала обрабатываются разными функциями, а только потом попадают в запрос. И тут оказывается, что про кавычки забыли.

Ошибок переполнения буфера в MySQL неизвестно, если будет слишком длинный запрос, может не выполниться просто.
16.02.2009, 15:10
Ответить
NO USERPIC

Sign

Может просто стоит использовать PDO ?
Вас не только проблемы инъекций будут волновать мало, но и в некоторых случаях заметно увеличится производительность сервера...
$sth = $dbh->prepare('SELECT count(*) FROM users WHERE userid=?');
$sth->execute(array($_GET['userid']));
// or
$sth = $dbh->prepare('SELECT count(*) FROM users WHERE userid=:userid');
$sth->execute(array(
  ':userid' => $_GET['userid']
));

P.S. Ну и вообще в PDO много вкусностей ; )
23.02.2009, 23:25
Ответить
NO USERPIC

rgbeast

Prepare statement можно использовать также в mysqli (функции mysqli_prepare() и др). Это действительно решает проблему SQL-инъекций ценой увеличения числа запросов к базе данных. Выйгрыш в производительности от PREPARE будет только в том случае, если одно подготовленное выражение будет использоваться несколько раз.

Тем не менее, много старого и нового кода не использует PREPARE и проблема инъекций остается актуальной.
24.02.2009, 00:20
Ответить
NO USERPIC

Sign

rgbeast
...ценой увеличения числа запросов к базе данных
Кажется Вы слегка сгущаете краски.. ; )
Конечно в PDO приходится указывать имя схемы в конекшене, что несколько затрудняет работу с несколькими схемами, но вьюшки и процедуры легко это решают без увеличения количества запросов.
С mysqli никогда серьёзно не работал, но там есть ещё и возможность множественных запросов - mysqli_multi_query...

Но, конечно же, не могу не согласиться что проблема актуальна
25.02.2009, 01:13
Ответить
NO USERPIC

rgbeast

Я имел в виду, что PREPARE - отдельный SQL-запрос к базе данных (возможно, реализация в PDO устроена иначе, но логично сделать именно так).
25.02.2009, 03:45
Ответить
Добавить комментарий
Отображение комментариев: Древовидное | Плоское
© 2007—2010 webew.ru
Сайт использует Flede и соответствует стандартам WAI-WCAG 1.0 на уровне A.
Rambler's Top100
Реклама: Вам нравится принимать подарки! Другим тоже! Подумайте над этим...