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

SQL-инъекции

5 февраля 2009, 19:11

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', или стандартную страницу с профилем пользователя, в противном случае. Перебором определяется первая буква пароля, затем вторая и.т.д.

.

Выводы

  • Все запросы, использующие внешние данные, требуется защитить от SQL-инъекций. Внешние данные могут быть переданы не только в качестве GET-параметров, но и методом POST, взяты из COOKIE, со сторонних сайтов или из базы данных, в которую пользователь имел возможность занести информацию.
  • Все числовые параметры следует явно преобразовывать в числовой вид с помощью функций intval() и floatval()
  • Все строковые параметры следует экранировать с помощью mysql_real_escape_string() и заключать в кавычки.
  • Если построить SQL-инъекцию сложно, не следует ожидать, что злоумышленник не догадается как это сделать. Особенно это относится к движкам, исходный код которых является публичным.

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

Статья написана по материалам онлайн-курса «Программирование на 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 с ошибками переполнения буфера?
16.02.2009, 11:39
Ответить
NO USERPIC

rgbeast

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

Ошибок переполнения буфера в MySQL неизвестно, если будет слишком длинный запрос, может не выполниться просто.
16.02.2009, 12: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, 20:25
Ответить
NO USERPIC

rgbeast

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

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

Sign

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

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

rgbeast

Я имел в виду, что PREPARE - отдельный SQL-запрос к базе данных (возможно, реализация в PDO устроена иначе, но логично сделать именно так).
25.02.2009, 00:45
Ответить
NO USERPIC

afobaz

Отличная статья-пособие для тех, кто поднимает свой портал
13.03.2011, 13:24
Ответить
NO USERPIC

bvn-2

Спасибо за статью, но тут есть подводные камни - использование магических кавычек.
Если кому интересно, можете подробнее почитать об инъекциях и защите от них на http://vova-beg.ru/tezam/detail.php?id=10
14.10.2011, 10:41
Ответить
NO USERPIC

rgbeast

Магические кавычки удобнее всего удалять централизовано в начале скрипта, как описано в статье: http://webew.ru/articles/198.webew
В этом случае не будет пересечения кода, работающего с sql-запросами и с магическими кавычками и не будет риска забыть о них.
14.10.2011, 10:46
Ответить

1234ru

Знаете..

Я не сторонник анархии, но мне в последнее время кажется, что SQL-инъекции - это 99% паранойя.

Конечно, мы должны быть на 100% уверены, что наш код делает именно то, что мы от него ждем.

Но на практике.
Вот запрос:

$sql = "SELECT * FROM users WHERE id = $_GET[user_id]";


Чего можно добиться инъекцией в него?

Точка с запятой не сработает, т.к. mysql_query() выполняет не больше одного запроса за раз.
Максимум - это подсунуть UNION, методом перебора угадав число колонок.
И то запрос этот выполняется где-то внутри скрипта. Максимум что получится - поломать скрипт, чтобы он нормально не отработал (показал скрипт хакеру ошибку - какая жалость).

Допустим, хакеру сказочно повезло и он наткнулся на скрипт, который оканчивается пользовательской переменной (если там есть что-то еще, получится запрос с ошибкой синтаксиса, и опять же нифига видно не будет), при этом возвращает в чистом виде результат запроса через echo (пускай, возвращает json для AJAX).
Методом перебора он подберет число колонок и сможет как-нибудь выдрать, например, хэш пароля (который ему не поможет, если пароль длиннее 7 символов) или ФИО пользователя.

Но это при удачном расположении звезд.
А так получит он шиш. Потому что он скриптов не видит (если речь, конечно, не идет о типовой CMS). Это просто обреченная на неудачу работа, по-моему, и в реальности такого бояться не стоит.
Я в последнее время все больше склоняюсь к тому, что мне впадлу писать intval() вокруг чисел, приходящих из $_GET.
То, что не убивает нас, делает нас инвалидами.
14.10.2011, 21:55
Ответить
NO USERPIC

rgbeast

Не согласен с нескольких позиций. Во-первых писать культурно лучше, чем некультурно. Если PHP не защищает от инъекций, нужно писать intval или применять всегда подход prepare, execute. Часто исходный код известен по той или иной причине (либо общая CMS, либо код стал доступен третим лицам, имевшим когда-то отношение к проекту). Если здесь допускать небрежность, то небрежность еще в одном месте позволит взломать (чаще всего именно двойные ошибки приводят к уязвимости). Phpbb, например, на моей памяти ломали очень много раз именно с помощью инъекций (в одном из этих случаях использовалась также вторая ошибка, что хэш пароля можно записать в куки и залогиниться).

Во-вторых, извлечь, например хэш пароля можно почти всегда при инъекциях методом AND password like 'a%'. У кого-нибудь из модераторов наверняка окажется подбираемый (при известном хэше) пароль (кому-нибудь будет влом делать длинный пароль, так как это чистая паранойя).
14.10.2011, 22:32
Ответить

1234ru

Ну, запросы к секьюрным таблицам - куда ни шло.

Но бывают запросы к служебным, которые не несут никакой полезной информации.

На самом деле, высказанное мной выше нельзя назвать устоявшейся точкой зрения.
Это, скорее, отражение недовольства в случае, когда я в очередной раз вынужден набирать длиннющщее название функции mysql_real_escape_string(), думая при этом: "Ну и чё сюда можно заинъектить?"

Кстати. Что такое подход prepare, execute?
То, что не убивает нас, делает нас инвалидами.
15.10.2011, 01:07
Ответить
NO USERPIC

rgbeast

PREPARE, EXECUTE, реализовано, например в Perl DBI, см. пример здесь: http://www.perl.com/pub/1999/10/DBI.html
Если подготовить запрос с помощью PREPARE, а потом с помощью EXECUTE ... USING выполнить, то пользовательская информация передается EXECUTE, что исключает ее попадание в область операторов SQL и не требует экранировки (в случае Perl DBI). Твое недовольство связано с изначальной непродуманности библиотеки PHP, которая заставляет писать (не забывать) лишние функции, по умолчанию не обеспечивая безопасность.
15.10.2011, 07:52
Ответить

1234ru

Ага, понятно насчет prepare-execute.

На мой взгляд, при использовании PHP-way работа с отдельным запросом лучше локализована. Я вижу запрос непосредственно при его составлении, что приводит к большей наглядности.

Так тут тоже возникло бы недовольство, и еще неизвестно, какое из недовольств больше :)

Подход prepare-execute выигрывал бы по удобству при использовании шаблонного запроса (т.е. когда для многих записей выполняется один и тот же запрос, меняется только какой-то аргумент), но использовать по запросу для каждой записи - плохая практика в принципе (по соображениям производительности), так что это преимущество по факту роли не играет.

Кстати (сейчас вспомнил). Для PHP есть библиотеки, реализующие подход prepare-execute.
То, что не убивает нас, делает нас инвалидами.
15.10.2011, 22:26
Ответить
NO USERPIC

artem_tsar

Да уж. Что правда, то правда. Досадно конечно, что в любой системе, созданной человеком, существуют дыры. Я вот, например, когда программировать учился, сразу начал с того, что вбивал в привычку при написании кода всегда использовать двойные и одинарные кавычки + функцию mysql_real_escape_string(); Теперь вот все автоматом делается, а думал, что никогда не привыкну. Наверное тем, у кого такой привычки не было изначально, то сложновато перестраиваться на написание валидного кода.
ЦАРЬ
11.01.2012, 04:28
Ответить
NO USERPIC

rgbeast

Система может быть изначально спроектирована исходя из требований безопасности, тогда такие ошибки будут редкостью, но написание скриптов усложнится. В PHP пошли по пути упрощения написания скриптов с одновременным усложнением написания безопасных скриптов. Чтобы указанная проблема не возникала, можно всегда использовать PREPARE или аналогичную систему абстракции.
11.01.2012, 14:30
Ответить
NO USERPIC

MeydanAz

function format_str(&$string)
    {
        return $string = htmlspecialchars(addslashes($string));
    }
    function format_arr(&$array)
    {
        foreach ($array as &$value)
        {
            $value = htmlspecialchars(addslashes($value));
        }
        return $array;
    }
    function format_string(&$string)
    {
        if (is_array($string))
        return format_arr($string);
        else format_str($string);
    }
array_walk($_REQUEST,"format_string");
array_walk($_POST,"format_string");
array_walk($_GET,"format_string");
05.12.2012, 05:06
Ответить
NO USERPIC

rgbeast

Превращать все параметры GET, POST и REQUEST в htmlspecialchars - нельзя считать корректеым решением проблемы безопасности. Экранировать строки нужно прямо перед вставкой в базу данных, причем достаточно mysql_real_escape_string. htmlspecialchars может испортить данные, для которых это преобразование не является естественным.
05.12.2012, 06:56
Ответить
Добавить комментарий
Отображение комментариев: Древовидное | Плоское
© 2008—2017 webew.ru, связаться: x собака webew.ru
Сайт использует Flede и соответствует стандартам WAI-WCAG 1.0 на уровне A.
Rambler's Top100

Реклама: