(ч1. Подделка межсайтовых запросов; ч3. Работа с пользовательским вводом)

Друпал предоставляет свои собственные средства для доступа к базе данных.

Во-первых, это позволяет не зависеть от используемого типа СУБД. К слову, на сегодняшний момент, полностью функционирует прослойка для MySQL и PostgreeSQL. В седьмом Друпале этот список будет расширен Ораклом и SQLite.

Во-вторых же, прослойка БД позволяет защититься от SQL инъекций.

Самая первая функция, о которой следует узнать при работе с базой — db_query().

Начну, пожалуй, с примера, в стиле которого пишут почти все начинающие друпаллеры:

/**
 * Пример 1 - небезопасный
 * Пример должен отобразить список заголовков нод типа $type (например, поступающего из поля формы)
 */
$result = db_query("SELECT nid, title FROM node WHERE type = '$type'");

$items = array();
while ($row = db_fetch_object($result)) {
 $items[] = l($row->title, "node/{$row->nid}");
}
return theme('item_list', $items);


В этом примере сразу несколько вещей в корне неправильны.

Псевдонимы названий таблиц



Название таблиц следует заключать в фигурные скобки, а также присваивать им псевдонимы, которые рекомендуется всегда использовать при обращении к колонкам. Измененный вызов будет выглядеть так:

$result = db_query("SELECT n.nid, n.title FROM {node} n WHERE n.type = '$type'");


Что нам это даст? Это обеспечит простоту обработки таблиц с префиксами. То есть, если у вас все таблицы в базе называются «pr_node», «pr_users» и т.д., Друпал автоматически будет подставлять корректные префиксы к таблицам, заключенным в скобки. Указание псевдонимов при этом избавит от надобности использовать фигурные скобки больше одного раза.

Фильтрация аргументов



Отсутствует фильтрация аргументов запроса. Это прямой путь к SQL инъекции. Если в $type окажется значение story' UNION SELECT s.sid, s.sid FROM {sessions} s WHERE s.uid = 1/*, то весь запрос будет уже таким:

SELECT n.nid, n.title FROM {node} n WHERE n.type = 'story' UNION SELECT s.sid, s.sid FROM {sessions} s WHERE s.uid = 1/*'


что позволит мошеннику завладеть айдишниками сессий, и в свою очередь, при создании корректной куки сессии, получить прямой админский доступ к сайту.

Защититься от этого довольно просто, используя параметризацию запроса. При формировании запроса, Друпал использует синтаксис функции sprintf. В строке запроса вставляются заглушки, которые заменяются параметрами, которые идут отдельно. При этом параметры проходят проверку и экранирование, так что вы можете забыть об инъекциях, используя данный подход. Вот некоторые примеры:

db_query("SELECT n.nid FROM {node} n WHERE n.nid > %d", $nid);
db_query("SELECT n.nid FROM {node} n WHERE n.type = '%s'", $type);
db_query("SELECT n.nid FROM {node} n WHERE n.nid > %d AND n.type = '%s'", $nid, $type);
db_query("SELECT n.nid FROM {node} n WHERE n.type = '%s' AND n.nid > %d", $type, $nid);



Список заменителей:

  • %d — для целых чисел (integers)
  • %f — для чисел с плавающей запятой, т.е. дробных (floats)
  • %s — для строк (однако, обратите внимание, что в запросе, вокруг строки выставляются кавычки)
  • %b — двоичные данные (не нужно оборачивать в кавычки)
  • %% — заменяется на % (например, для LIKE %monkey%)


Для конструкций IN (... , ... , ...), используйте функцию db_placeholders(), которая создаст нужную последовательность заменителей, по заданному массиву параметров, например:

$nids = array(1, 5, 449);
db_query('SELECT * FROM {node} n WHERE n.nid IN ('. db_placeholders($nids) .')', $nids);



Если вы используете модуль Devel, у вас есть очень простой способ получения конечных запросов для отладочных целей. Просто вызовите функцию db_queryd() с точно такими же параметрами, как вы вызываете db_query().


Теперь, наш запрос будет выглядеть так:

$result = db_query("SELECT n.nid, n.title FROM {node} n WHERE n.type = '%s'", $type);


Ранжирование результатов запроса



Наш пример на большом сайте выведет большущий список нодов. Что если нам можно ограничиться всего первым десятком? Первым позывом будет использовать SQL конструкцию LIMIT, н��пример

SELECT n.nid, n.title FROM {node} n WHERE n.type = '%s' LIMIT 0, 10


и вроде бы все хорошо, но на Postgree SQL этот код приведет к ошибке, так как с этим сервером управления, вам нужно использовать конструкцию OFFSET 0 LIMIT 10. А еще на каком-нибудь Оракле, синтаксис опять другой. Что же делать?

Ответ — использовать db_query_range() для лимитирования количества результатов запроса. Его использование аналогично db_query, за исключением того, что в после всех аргументов, вам нужно указать два параметра — номер первой строки, и количество результатов. Наш запрос преобразится в следующее:

// выведет первых 10 результатов
$result = db_query_range("SELECT n.nid, n.title FROM {node} n WHERE n.type = '%s'", $type, 0, 10);



И на последок, если вам ко всему еще нужен постраничный вывод, используйте функцию pager_query(). Она отличается от db_query_range() наличием всего одного необязательного параметра, о котором вы можете почитать на странице документации. С этой функцией вывод листалки страниц прост как дважды два:

/**
 * Пример 2 - безопасный, с листалкой
 */
// изменяем сам запрос
$result = pager_query("SELECT n.nid, n.title FROM {node} n WHERE n.type = '%s'", $type, 0, 10);

$items = array();
while ($row = db_fetch_object($result)) {
 $items[] = l($row->title, "node/{$row->nid}");
}

$output = theme('item_list', $items);

// добавляем листалку
$output .= theme('pager');

return $output;



Как видите, всего две строчки изменений. Всю рутину по подхватыванию текущей страницы, обработке и т.д. полностью берет на себя Друпал.

Возможность изменения запроса модулями



Довольно часто имеет смысл предоставить другим модулям возможность повлиять на ваш запрос. В Друпале это реализуется связкой функции db_rewrite_sql(), и реализациями хука hook_db_rewrite_sql() в модулях. Наш запрос будет выглядеть так:

$result = pager_query(db_rewrite_sql("SELECT n.nid, n.title FROM {node} n WHERE n.type = '%s'", 'n', 'nid'), $type, 0, 10);


а вот и пример реализации хука, для того, чтобы у вас было представление, что происходит:

// Модуль отсеет все ноды, авторы которых сутки не были на сайте
function my_module_db_rewrite_sql($query, $primary_table, $primary_field, $args) {
 switch ($primary_field) {
  case 'nid':
   if ($primary_table == 'n') {
    $return['join'] = "LEFT JOIN {users} u ON $primary_table.uid = u.uid";
    $return['where'] = 'u.login > '. time() - 60 * 60 * 24;
   }
   return $return;
   break;
 }
}



Возвращенные из хука 'join' элементы, будут прикреплены к нашему запросу, 'where' — добавлены к списку условий, и наш запрос после обработки будет таким:

SELECT n.nid, n.title FROM {node} n LEFT JOIN {users} u ON n.uid = u.uid WHERE n.type = '%s' AND u.login > 199976743


После этого, он, собственно, поступит в pager_query() и будет обработан как обычно.

Финальный код примера



/**
 * Пример 3 - безопасный, с листалкой и возможностью перезаписи запроса
 */
// добавляем db_rewrite_sql
$result = pager_query(db_rewrite_sql("SELECT n.nid, n.title FROM {node} n WHERE n.type = '%s'", 'n', 'nid'), $type, 0, 10);

$items = array();
while ($row = db_fetch_object($result)) {
 $items[] = l($row->title, "node/{$row->nid}");
}

$output = theme('item_list', $items);

$output .= theme('pager');

return $output;


* This source code was highlighted with Source Code Highlighter.


Полезные ссылки


Статьи цикла «Безопасный код»