Как стать автором
Обновить

Панацея от SQL-инъекций — запросы с параметрами

Время на прочтение4 мин
Количество просмотров7.6K

Введение


Этот топик — начало небольшого цикла о панацеях от различных уязвимостей Web-приложений.

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

Предлагаю им, да и вообще всем использовать так называемые prepared-statements. Их еще называют параметризованные запросы.
Идея проста как и все гениальное — отделить запрос от пользовательских данных.
Вы, наверно, спросите: что означит «отделить»? Имеется в виду: отделить их от составления запроса.
Ведь ваш сайт должен обрабатывать пользовательские данные, а не составлять на их основе SQL-запросы (хотя кто знает?). Последнее даже звучит глупо, но именно из-за этого мы имеем подобную ситуацию.

Реализация


Реализация не сложнее идеи. Для примера возьмем запрос получения записи блога:
SELECT `date`,`title`,`text`,`tags` FROM `posts` WHERE `url_title`='bla-bla-bla' <br/>

В PHP-коде это будет выглядеть примерно так:
<?php<br/>    //...<br/>    $sql = 'SELECT `date`,`title`,`text`,`tags` FROM `posts` WHERE `url_title`=\''.$_GET['url_title'].'\'';<br/>    DB::exec($sql);<br/>    //... <br/>

Невооруженным глазом видна проблема в безопасности — параметр url_title адресной строки не фильтруется.
Сей факт обычно обнаруживается тогда, когда его уже кто-нибудь из посетителей нашел. B тогда код преобразуется в более ужасный вид:
<?php<br/>    //...<br/>    $sql = 'SELECT `date`,`title`,`text`,`tags` FROM `posts` WHERE `url_title`=\''.mysql_real_escape_string($_GET['url_title']).'\'';<br/>    DB::exec($sql);<br/>    //... <br/>

Некоторые запросы выглядят особо ужасно:
<?php<br/>    //...<br/>    $sql = 'SELECT `firstname`,`lastname`,`nickname`,`avatar` FROM `users` WHERE `login`=\''.mysql_real_escape_string($_GET['username']).'\' AND `password`=\''.mysql_real_escape_string($_GET['password']).'\'';<br/>    DB::exec($sql);<br/>    //... <br/>

Я даже, когда печатал этот безопасный пример, ошибся и допустил синтаксическую ошибку где-то в районе кавычек и экранирующих слешей.
Чем больше запрос, тем больше неразбериха. В глазах рябит и совсем не читается.

Превратим теперь оба запроса в параметризованные.
<?php<br/>    //...<br/>    $sql = 'SELECT `date`,`title`,`text`,`tags` FROM `posts` WHERE `url_title`=:1';<br/>    DB::exec($sql, $_GET['url_title']);<br/>    //... <br/>

<?php<br/>    //...<br/>    $sql = 'SELECT `firstname`,`lastname`,`nickname`,`avatar` FROM `users` WHERE `login`=:1 AND `password`=:2';<br/>    DB::exec($sql, $_GET['username'], $_GET['password']);<br/>    //... <br/>

По-моему, очень удобно. Вместо конструкций :N подставляется соответствующий аргумент. Их, конечно же, можно дублировать и все такое.
А что происходит за кулисами (код взят из одного проекта, где я и придумал[хоть идея и не моя, но я об этом не знал] это)?

public static function exec($query) {<br/>    global $args;<br/>    if(func_num_args() > 1) {<br/>        $args = func_get_args();<br/>        $query = preg_replace_callback(<br/>            '/:([0-9]+)/',<br/>            create_function(<br/>                '$matches',<br/>                'global $args; return "\'".str_replace("\'", "\\\\\'", @$args[$matches[1]])."\'";'<br/>            ),<br/>            $query<br/>        );<br/>    }<br/>    self::$result = sqlite_query($query, self::$handle);<br/>    return self::$result;<br/>} <br/>

Конечно, регулярные выражения — не лучший выбор в плане производительности, но меня это устраивает. Можно приделать выборку из глобального контекста типа :glob:varname (например, :glob:_GET[id]), но для этого нужен парсер по-умней — в моих целях создание онного не числится. Или отдельное присвоение параметров как в Yii. Однако, это уже дело вкуса. По-моему то, что сделано Yii не решает вопрос читабельности, напротив:
$username = mysql_real_escape_string($username); <br/>
удобнее, чем
$command->bindParam(":username",$username,PDO::PARAM_STR); <br/>

Достоинства

  • Повышается читабельность
  • Не требуется отдельная функция, отвечающая за фильтрацию
  • Если принять такое обращение в БД за стиль, то про SQL-инъекции можно забыть.

Недостатки

  • В начале придется помучаться, чтобы перейти на использование таких запросов
Теги:
Хабы:
Всего голосов 26: ↑12 и ↓14-2
Комментарии38

Публикации