

Друпал предоставляет свои средства для доступа к базе данных. Это, во-первых, позволяет не зависеть от конкретного типа СУБД, а во-вторых, защититься от 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);Список заменителей:
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;# | rodman
Дополнительно в разделе описания db_rewrite_sql() - думаю было бы полезно описать в каких случаях стоит применять эту функцию разработчикам модулей.
db_rewrite_sql() - применяется если запрос используется например для выборки материалов, т.е. разработчик должен предположить что этот конкретный запрос затем захотят изменить другие разработчики модулей для изменения прав доступа к нодам(фактически именно для этих целей функция и была придумана).
Уверен что автор статьи хорошо понимает о чем я говорю, все таки неплохо было бы этот момент описать.
не хочу быть навязчивым, но в заголовке статьи следует большими буквами написать что статья относится только к DRUPAL.
И не имеет отношения к общим принципам безопасной работы с БД.
Также следует сделать сноску - желательно в первом обзаце что по-настоящему обезопасить себя от проблем можно
только с помощью prepared statement - стандарт работы с БД.
Сделать это следует для того, чтобы молодые горячие (и неопытные) головы не восприняли
данные советы на веру как руководство к действию на все случае жизни.
В большой жизни всё сложнее (и интереснее).
То, что статья была размещена здесь, напрямую означает, что она относится к Друпалу :)
Однако, вы правы на счет prepared statement. Я упомяну в статье, что плейсхолдеры являются аналогами этого подхода.
Вы всё верно говорите.
Проблема в том что статья действительно толково написана.
На таких статьях учатся и воспринимают всё на веру не только пользователи Drupal.
Успехов
Кстати, в 7.х версии переходят на prepared statements. И, судя по исходникам, на сегодняшний день переведено процентов 50-60 кода.
Так что статью в любом случае придется переписывать :-)
Ссылки с других сайтов
Все молчат как партизаны.