Вт 5 Дек 2006
SQL-инъекции: борьба в удовольствие
Артём и Максим Категория: Наши лучшие посты, Техплатформа PHP
Прочитать позже:
Опасность SQL-инъекций зачастую недооценивают. А даже если и знают в них толк, то нередко оставляют мероприятия по защите «на потом». И напрасно, поскольку существует довольно простой способ защиты. Этот способ, помимо собственно защиты, позволяет чрезвычайно удобно собирать тексты SQL-запросов из констант и переменных, не прибегая ни к утомительной работе с одинарными и двойными кавычками, ни к объектным способам доступа к БД.
Если скрипты уже написаны, то ничего не стоит изменить в них глобальной автозаменой всего одну функцию (обратная совместимость сохраняется). Но лучше всё же использовать приведённую методику с самого начала, тем более. Что она ещё и существенно упрощает отладку кода.
В чём опасность?
SQL-инъекция - один из распространённых способов взлома сайтов, позволяющий злоумышленнику исполнить практически любой SQL-запрос. Например, если URL страницы имеет вид
http://life.screenshots.ru/index.php?login=vasja
то вместо vasja можно написать нечто вроде
vasja'; DELETE FROM SomeTable --
предварительно приведя спецсимволы к виду %xx. Тогда запрос, формирующий страницу, вида
$query="SELECT * FROM WebsiteUsers WHERE UserName='" . $_GET["login"] . "'";
будет выглядеть как
SELECT * FROM WebsiteUsers WHERE ID='vasja'; DELETE FROM SomeTable --‘
что приведёт к очистке таблицы (последовательность — позволяет игнорировать завершающую кавычку, поэтому синтаксической ошибки не возникает). Другое дело, что у логина, под которым PHP обращается к SQL-серверу, может не быть прав на очистку таблицы либо запись двух операторов может быть запрещён конкретным SQL-диалектом. Но сути это не меняет, поскольку это лишь один из множества возможных примеров SQL-инъекций.
Подробнее можно почитать на Wikipedia или любых хакерских сайтах.
Как люди борются?
Есть несколько типовых вариантов борьбы с инъекциями вредоносного кода. Простейший - стандартная функция mysql_escape_string и подобные ей, которые заэскейпливают символы-разделители вроде точки с запятой, кавычки и т.п. Но такой метод не спасает от вставки всяких управляющих слов вроде OR, AND и т.п. Более того, нет гарантии, что ваш сервер БД поддерживает эскейп-последовательности символов.
Второй способ заключается в отлавливании известных вариантов инъекций, например, тех же слов OR и AND. Но, во-первых, иногда требуется передать запросу как раз такую последовательность в штатном режиме (а отключать в таких случаях защиту - ещё больший повод для путаницы). А, во-вторых, перебирать сотню вариантов - тупиковый путь, поскольку всегда найдётся сто первый.
Третий способ - это использование объектных «обёрток», когда параметры запроса «скармливаются» объекту при помощи присваивания значений свойствам или вызовов отдельных методов. Этот путь хорош всем, за исключением его громоздкости (а классы работы с БД почти всегда слоноподобны).
Есть способ лучше
Собственно, основная проблема SQL, которая приводит к возможности SQL-инъекций, заключается в отсутствии контроля типов передаваемых значений параметров запроса. Поэтому решение очевидно - параметры нужно предварительно типизировать!
Мы предлагаем использовать одну-единственную короткую и несложную функцию, которой нужно заменить вызовы mysql_query. В принципе, вам не составит труда написать её самостоятельно, но свои варианты на PHP и ASP мы написали довольно давно и за время эксплуатации добавили немало удобств и поисправляли ошибок.
Формат вызова:
xquery(QueryTemplateString, Parameter1, Parameter2...);
Функция возвращает то же, что и обычный mysql_query: в случае неудачного исполнения false, в случае удачного - recordset (для запросов, возвращающих наборы данных, например, SELECT-запросов) либо true (для запросов на изменение данных).
Сама строка шаблона - это обычная строка запроса, в которой, однако, в местах, где требуется вставить значение параметра, стоят специальные маркеры. Например:
$Rcs=mysql_query("SELECT * FROM WebsiteUsers WHERE Age>" . $_GET["age"] . " OR UserName='" . $_GET["login"]);
заменяется на:
$Rcs=хquery("SELECT * FROM WebsiteUsers WHERE Age>^N OR UserName=^S", $_GET["age"], $_GET["login"]);
При помощи маркеров мы установили, что возраст - это числовое значение (^N), а логин - строковое (^S).
Процедура проанализирует типы переданных параметров (поскольку и в PHP, и в JScript есть возможность доступа к переданным функции параметрам как к элементам предопределённого массива, мы можем указывать любое количество параметров).
Для первого тип будет приведён к числовому (и вставить управляющие символы или слова будет невозможно). Для второго просто будут заэскейплены (либо заменены на другие - зависит от диалекта SQL) кавычки, которые могли бы разорвать текст запроса, а также добавлены кавычки, обрамляющие полученную строковую константу. Полученная в итоге безопасная SQL-строка будет передана на обработку mysql_query.
Собственно, вот и вся идея. А дальше начинается хронический улучшай.
Поддержка нескольких соединений
В JScript, например, очень удобно сделать два синтаксиса:
xquery(QueryTemplateString, Parameter1, Parameter2...);
xquery(ConnectionObject, QueryTemplateString, Parameter1, Parameter2...);
Если тип первого параметра - object, то используется второй синтаксис, где функции передаётся объект соединения (нередка ситуация, когда работа ведётся с более чем одним сервером БД), в противном случае используется первый синтаксис и соединение берётся по умолчанию (например, из глобальных параметров сайта – opt_conn).
Поддержка префиксации таблиц
Нередка ситуация, когда необходимо в одной физической БД хранить таблицы, которые, по-хорошему, должны бы принадлежать разным БД. Например, это актуально, если хостинг-провайдер предоставляет ограниченное число БД (и требует деньги за возможность создания дополнительных БД). Или же иногда возникает возможность сделать один запрос к двум таблицам, принадлежащим разным БД – и сделать это технически непросто.
Поэтому большинство тиражируемых web-приложений (например, блоговый «движок» Wordpress) при установке запрашивает, какой префикс дописать к идентификаторам всех своих таблиц БД - и становится возможным установить на одной БД сколько угодно таких движков.
Решение снова лежит на поверхности. Вводим новый маркер ^@ и ещё один глобальный параметр сайта (напомним - первый был объектом соединения с БД) - префикс таблицы:
$Rcs=xquery("SELECT * FROM WebsiteUsers WHERE UserName=^S");
трансформируется в
$Rcs=xquery("SELECT * FROM ^@WebsiteUsers WHERE UserName=^S");
Функция заменит все маркеры ^@ на префикс, и реально обращение произойдёт уже не к WebsiteUsers, а к таблице someprefix_WebsiteUsers, где someprefix - значение того самого глобального параметра (opt_table_prefix). При этом имя таблицы для пишущего код программиста по-прежнему соответствует имени, указанном в его ER-диагрмме.
Поддержка отладки
Очень удобно, когда вы можете увидеть подробную отладочную информацию, включая собранный текст SQL-запроса, который потом можно вставить в консоль сервера.
Для этого достаточно ввести ещё один глобальный параметр (opt_debug_mode, а для PHP-реализации ещё и opt_debug_show_sql, в котором разрешать либо запрещать вывод на поток отладочной информации.
Поддержка дополнительных типов
Ничто не мешает добавить обработку дополнительных типов параметров (нам мешает нехватка времени - но как-нибудь обязательно надо будет сделать, а пока опишем идеи).
Помимо этого, можно ввести дополнительные ограничения типов, например, ^S[50], которое будет автоматически обрезать строковой параметр до длины в 50 символов (согласитесь, это удобнее, чем обрезать отдельным substr).
Подобным же образом (^D, ^D[hh-MM-yyyy] - в каком формате подаётся значение параметра) можно автоматизировать весьма утомительную работу с датами/временем.
При помощи ^0 можно удобно трансформировать значения переменных null, undefined и т.п. в IS NULL или IS NOT NULL
При помощи ^M[regexp] можно контролировать соответствие входного строкового параметра самым сложным правилам - тогда xquery будет возвращать false в случае несоответствия.
Ну, наверное, идею вы поняли.
Где взять исходники?
Нашими исходниками вы можете пользоваться по своему усмотрению, не забывая на нас ссылаться. Если у вас есть идеи по улучшению и исправлению, то присылайте их нам - будем весьма признательны.
В настоящее время PHP- и JScript-версии несколько различаются по функциональности, но мы надеемся, что через некоторое время их уравняем.
декабря 6, 2006 at 02:53
“Простейший - стандартная функция mysql_escape_string и подобные ей, которые заэскейпливают символы-разделители вроде точки с запятой, кавычки и т.п. Но такой метод не спасает от вставки всяких управляющих слов вроде OR, AND и т.п. Более того, нет гарантии, что ваш сервер БД поддерживает эскейп-последовательности символов.”
Во-1, лучше использовать mysql_real_escape_string(), который доступен с 4.3.0+. Во-2, причём тут OR, если строки так и останутся строками, управляющие слова не повлияют на запрос. В-3х, для каждой БД своя функция, которая делает то, что понимает БД. Поэтому этот метод правильный.
“иногда требуется передать запросу как раз такую последовательность [OR/AND] в штатном режиме (а отключать в таких случаях защиту - ещё больший повод для путаницы)”
Передавать от пользователя часть запроса с AND не нужно никогда. Передавать от программы нужно, это должна понимать функция-обвёртка, но проблемы это не составляет.
“а классы работы с БД почти всегда слоноподобны”
Это необходимое требование класса? :) Можно всегда написать простой.
Про код.
1) Вместо простого return false; лучше поднимать исключение.
2) str_replace(”‘”, “`” …)
Плохо, что меняется сам текст. С точки зрения типографики апостроф ‘ (U+0027) и гравис ` (U+0060) очень разные символы. Если используется MySQL, то надо применять mysql_escape_string(), если используется MSSQL, то str_replace(”‘”, “”” …), и т.д.
3) Кто писал, что классы громоздки? Эта функция тоже будет не маленькая, особенно с поддержкой большого количества типов. Лучше обработку ошибок пользователя делать на уровне выше, а не на уровне доступа к БД. Поэтому вооружаемся бритвой Оккама и следим, чтобы при любом возможном синтаксисе строка запроса читалась просто и быстро.
P.S. Здесь в исходниках вырезаны знаки больше и меньше. Очень символично ;)
декабря 6, 2006 at 12:51
“Во-1″: если параметр запроса не строковой, а числовой, то проблема актуальна:
…WHERE ID=5 OR 1=1
может вывести какие-то данные, не предназначенные для публичного просмотра (т.е. вместо пятёрки в запрос передали 5 OR 1=1)
“Во-2″: имеется в виду, что запросу передаётся некий текст, в котором пользователь так и набрал: His blogs’ and forums’ interfaces are really good. Вот пожалуйста: и кавычки, и AND.
“В-3″: мы имели в виду стандартные библиотеки вроде PearDB и PearADO, а не саму ООП-парадигму. Ничто не мешает написать эту функцию в объектном виде.
Замучались с оформлением исходников - и переложили их в отдельные файлы.
Спасибо за столь конструктивный комментарий.
декабря 12, 2006 at 23:34
Интересная информация! Спасибо!
января 23, 2007 at 23:44
Подобная идея реализована в библиотеке Дмитрия Котерова DbSimple http://dklab.ru/lib/DbSimple/
марта 9, 2007 at 00:23
Вы ребят, какой SQL юзаете, что он ; понимает как разделитель команд. Впервые такое вижу…
Например, mysql скажет на эту строчку следующее:
Sql Error(1064):’You have an error in your SQL syntax near ‘; DELETE FROM SomeTable –’ at line 1′
Если же вы тестируете на тулзе типа PHPMYADMIN или другого клиента MySQL, то как правило они разбирают ; сами.
Так что либо ваш пример не верен, либо вы тестировали это на каком-то другом движке. Напишите, на каком?
В любом случае, не надо людей пугать нереальными вещами
апреля 25, 2007 at 19:41
Сайт супер! мне понравился.
Администратору респект!
мая 1, 2007 at 17:00
В том и дело, устал читать про SQL-иньъекции где говорят вставим ; и напишем длае свой запрос.
Ну как Экспер вверху сказал mysql_query() не парсит сама ; символ и выдает ошибку. Какая инъекция тут? А для паскаля - хм, ну кто юзает паскаль для веба?Так что тут чистая теория. Уже устарела статья. А по поводу or and xor - это актуально. Соглашусь.
мая 23, 2007 at 00:10
Хочу сказать спасибо владельцам сайта за все. Так держать!!!
С уважением, Иванов Дмитрий
мая 29, 2007 at 22:57
Привет! Хочу сказать спасибо людям которые создали этот сайт. Вы лучшее!!!
Ваш постоянный посетитель :)
июня 2, 2007 at 09:26
подскажите, как вставить безопасно строку (INSERT INTO tb_name ..) в mysql. Функция mysql_escape_string() не помогает, выскакивает ошибка синтаксиса
августа 8, 2007 at 15:55
Весьма познавательно и поучительно. Для себя вынес полезную информацию.
Господа, зацикленные на MySQL, есть РСУБД несколько более иные. Это я про то, что не нужно пренебрежительно относиться к символу “;”. Тот же PostgreSQL скушает хоть десяток операторов и не подавится. А вот заслешить получаемые данные и привести к нужному типу — одно это мероприятие сведёт к минимуму инъекцию.
ноября 28, 2007 at 17:03
Hi.
Good design, who make it?
декабря 4, 2007 at 18:00
Привет всем, млин тут комп не давно сломался и остался я без компа и инета аж на целых 5 дней! Епт такой тошняк был пока не починил, ведь есть все таки зависимость от компьютора как ни крути. Помню раньше не было и не надо )). А еще игры это вообще жесть затягивает. Не давно видел объявление на ряду с лечением табакокурения, алкогализма, в третей строчки было лечение от ИГРОМАНИИ. Во как! Докатились!
Зависимость или свобода конечно все зависит от нас.
февраля 8, 2008 at 00:04
Ты любишь анальный секс? :) Я тоже…
февраля 9, 2008 at 21:54
Нужна помощь!!! Вот в чем проблеиа!
Скрин тут:
http://silverway.ru
февраля 11, 2008 at 15:53
Предлагается панель для регистрации доменных имён в зонах .RU и .SU имеющего лицензию от REG.RU регистратора - DomainReseller.ru
Тарифным планом “Премиум 100-390″ обеспечивается:
Регистация доменов Ru - 100 рублей, Su - 390 рублей!
На данный момент возможна регистрация доменов на физическое или юридическое лицо. Совсем скоро намечено подключение опции неограниченного количества суб-аккаунтов, которая каждому клиенту предоставит возможность создать свой собственный сайт на основе аналогичной панели, заводить своих собственных клиентов и формировать для них персональные тарифные планы.
Все домены, зарегистрированные на сайте DomainReseller.ru, всегда переносятся в любой аккаунт на сайте Reg.ru по запросу владельца.
Такая панель стоит 300 WMZ
domain@zaregi.ru
http://zaregi.ru/
февраля 13, 2008 at 11:49
Hello! Подскажите плиз где можно порно бесплатное найти!
июля 26, 2008 at 00:47
“Во-1″: если параметр запроса не строковой, а числовой, то проблема актуальна:
…WHERE ID=5 OR 1=1
может вывести какие-то данные, не предназначенные для публичного просмотра (т.е. вместо пятёрки в запрос передали 5 OR 1=1)
$_GET['id'] = (int)$_GET['id'];
какие AND и OR?????
марта 19, 2009 at 08:57
За такие посты надо награды давать, на полном серьезе!
октября 11, 2009 at 15:07
; - используется в MSSQL для разделения запросов. Там же используется “–” - для комментирования остатков кода справа.
декабря 22, 2009 at 07:18
<img src=data:text/html,%0D%0A%0D%0A alert(document.cookie)%3B%0D%0A %0D%0A%0D%0A>
декабря 25, 2009 at 15:11
$blackchars = “`~!@#$%^&*()_+-=[]{};:’\”,./\\№|”;
$blackwords = array(’select’, ‘union’, ‘order’, ‘where’, ‘char’, ‘from’, ‘drop’, ‘code’, ’script’);
//Обработка $_POST
for ($k = 0; $k < count($_POST); $k++) {
for($i = 0; $i < strlen($blackchars); $i++){
$_POST[key($_POST)] = str_replace($blackchars[$i], ”, $_POST[key($_POST)]);
}
for($i = 0; $i < count($blackwords); $i++){
$_POST[key($_POST)] = str_replace($blackwords[$i], ”, strtolower($_POST[key($_POST)]));
}
next($_POST);
}
//Обработка $_GET
for ($k = 0; $k < count($_GET); $k++) {
for($i = 0; $i < strlen($blackchars); $i++){
$_GET[key($_GET)] = str_replace($blackchars[$i], ”, $_GET[key($_GET)]);
}
for($i = 0; $i < count($blackwords); $i++){
$_GET[key($_GET)] = str_replace($blackwords[$i], ”, strtolower($_GET[key($_GET)]));
}
next($_GET);
}