ShvetsGroup

 

Безопасный код: Работа с базой данных

  • neochief's picture
0 comments

Безопасный код: Работа с базой данных

Статья эвакуирована с DrupalDance.com

Друпал предоставляет свои средства для доступа к базе данных. Это, во-первых, позволяет не зависеть от конкретного типа СУБД, а во-вторых, защититься от 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'", 10, 0, NULL, $type);
// обратите внимание на смену очередности параметров. Здесь 10 это кол-во результатов,
// а 0 - номер стратовой страницы, а не результата, как было в db_query_range
// затем NULL (так как нам не нужен особый запрос для вычисления количества) и лишь потом
// идет список параметров запроса.

$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'), 10, 0, NULL, $type);

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

// Модуль отсеет все ноды, авторы которых сутки не были на сайте
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'), 10, 0, NULL, $type);

$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;

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

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

Got anything to add?