Шаблонизация SQL-запросов и борьба с SQL-инъекциями 

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