
(ч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.