Статьи Sql-инъекции: борьба в удовольствие

Discussion in 'Статьи' started by FraiDex, 11 Jan 2008.

  1. FraiDex

    FraiDex Elder - Старейшина

    Joined:
    16 Jun 2006
    Messages:
    193
    Likes Received:
    68
    Reputations:
    -11
    Опасность SQL-инъекций зачастую недооценивают. А даже если и знают в них толк, то нередко оставляют мероприятия по защите «на потом». И напрасно, поскольку существует довольно простой способ защиты. Этот способ, помимо собственно защиты, позволяет чрезвычайно удобно собирать тексты SQL-запросов из констант и переменных, не прибегая ни к утомительной работе с одинарными и двойными кавычками, ни к объектным способам доступа к БД.
    Если скрипты уже написаны, то ничего не стоит изменить в них глобальной автозаменой всего одну функцию (обратная совместимость сохраняется). Но лучше всё же использовать приведённую методику с самого начала, тем более. Что она ещё и существенно упрощает отладку кода.
    В чём опасность?
    SQL-инъекция - один из распространённых способов взлома сайтов, позволяющий злоумышленнику исполнить практически любой SQL-запрос. Например, если URL страницы имеет вид

    http://life.screenshots.ru/index.php?login=vasja
    то вместо vasja можно написать нечто вроде

    Code:
    vasja'; DELETE FROM SomeTable --
    предварительно приведя спецсимволы к виду %xx. Тогда запрос, формирующий страницу, вида

    PHP:
    $query="SELECT * FROM WebsiteUsers WHERE UserName='" $_GET["login"] . "'";
    будет выглядеть как

    Code:
    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 мы написали довольно давно и за время эксплуатации добавили немало удобств и поисправляли ошибок.
    Формат вызова:

    PHP:
    xquery(QueryTemplateStringParameter1Parameter2...);
    Функция возвращает то же, что и обычный mysql_query: в случае неудачного исполнения false, в случае удачного - recordset (для запросов, возвращающих наборы данных, например, SELECT-запросов) либо true (для запросов на изменение данных).
    Сама строка шаблона - это обычная строка запроса, в которой, однако, в местах, где требуется вставить значение параметра, стоят специальные маркеры. Например:

    PHP:
    $Rcs=mysql_query("SELECT * FROM WebsiteUsers WHERE Age>" $_GET["age"] . " OR UserName='" $_GET["login"]);
    заменяется на:

    PHP:
    $Rcs=хquery("SELECT * FROM WebsiteUsers WHERE Age>^N OR UserName=^S"$_GET["age"], $_GET["login"]);
    При помощи маркеров мы установили, что возраст - это числовое значение (^N), а логин - строковое (^S).
    Процедура проанализирует типы переданных параметров (поскольку и в PHP, и в JScript есть возможность доступа к переданным функции параметрам как к элементам предопределённого массива, мы можем указывать любое количество параметров).
    Для первого тип будет приведён к числовому (и вставить управляющие символы или слова будет невозможно). Для второго просто будут заэскейплены (либо заменены на другие - зависит от диалекта SQL) кавычки, которые могли бы разорвать текст запроса, а также добавлены кавычки, обрамляющие полученную строковую константу. Полученная в итоге безопасная SQL-строка будет передана на обработку mysql_query.
    Собственно, вот и вся идея. А дальше начинается хронический улучшай.
    Поддержка нескольких соединений
    В JScript, например, очень удобно сделать два синтаксиса:

    PHP:
    xquery(QueryTemplateStringParameter1Parameter2...);
    PHP:
    xquery(ConnectionObjectQueryTemplateStringParameter1Parameter2...);
    Если тип первого параметра - object, то используется второй синтаксис, где функции передаётся объект соединения (нередка ситуация, когда работа ведётся с более чем одним сервером БД), в противном случае используется первый синтаксис и соединение берётся по умолчанию (например, из глобальных параметров сайта – opt_conn).
    Поддержка префиксации таблиц
    Нередка ситуация, когда необходимо в одной физической БД хранить таблицы, которые, по-хорошему, должны бы принадлежать разным БД. Например, это актуально, если хостинг-провайдер предоставляет ограниченное число БД (и требует деньги за возможность создания дополнительных БД). Или же иногда возникает возможность сделать один запрос к двум таблицам, принадлежащим разным БД – и сделать это технически непросто.
    Поэтому большинство тиражируемых web-приложений (например, блоговый «движок» Wordpress) при установке запрашивает, какой префикс дописать к идентификаторам всех своих таблиц БД - и становится возможным установить на одной БД сколько угодно таких движков.
    Решение снова лежит на поверхности. Вводим новый маркер ^@ и ещё один глобальный параметр сайта (напомним - первый был объектом соединения с БД) - префикс таблицы:

    PHP:
    $Rcs=xquery("SELECT * FROM WebsiteUsers WHERE UserName=^S");
    трансформируется в

    PHP:
    $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-версии несколько различаются по функциональности, но мы надеемся, что через некоторое время их уравняем.
    • Исходник xquery на JScript (IIS ASP) для MS SQLServer
    • Исходник xquery на PHP для mySQL
    Взято из life.screenshots.ru