Пользовательские таблицы базы данных: безопасность

22 января 2018

Это вторая часть серии о пользовательских таблицах базы данных в WordPress. В первой части мы рассмотрели причины и против использования пользовательских таблиц. Мы рассмотрели некоторые детали, которые нужно было бы рассмотреть - имена столбцов, типы столбцов, а также как создать таблицу. Прежде чем мы пойдем дальше, нам нужно рассмотреть, как безопасно взаимодействовать с этой новой таблицей. В предыдущей статье я рассмотрел общую санитарию и валидацию - в этом уроке мы рассмотрим это более подробно в контексте баз данных.

Безопасность при взаимодействии с таблицей базы данных имеет первостепенное значение - вот почему мы рассказываем об этом в начале цикла. Если это не сделано правильно, вы можете оставить свою таблицу открытой для манипуляции с помощью SQL-инъекции. Это может позволить хакеру извлекать информацию, заменять содержимое или даже изменять поведение вашего сайта - и ущерб, который они могут сделать, не ограничивается вашей пользовательской таблицей.

Предположим, мы хотим, чтобы администраторы удаляли записи из нашего журнала активности. Общей ошибкой, которую я видел, является следующая:

if ( !empty($_GET['action'])
     &&'delete-activity-log' == $_GET['action']
     &&isset($_GET['log_id']) ) {
          global $wpdb;
          unsafe_delete_log($_GET['log_id']);
}
function unsafe_delete_log( $log_id ){
     global $wpdb;
     $sql = "DELETE FROM {$wpdb->wptuts_activity_log} WHERE log_id = $log_id";
     $deleted = $wpdb->query( $sql );
}

Так что здесь не так? Много: они не проверяли разрешения, поэтому каждый может удалить журнал активности. Также они не проверяли nonces, поэтому даже при проверке разрешений пользователь-администратор может быть обманут в удалении журнала. Все это было рассмотрено в этом уроке. Но их третья ошибка объединяет первые две: функция unsafe_delete_log () использует переданное значение в команде SQL, не избегая ее вначале. Это делает его широко открытым для манипуляций.

Предположим, что его предполагаемое использование

www.unsafe-site.com?action=delete-activity-log&log_id=7

Что делать, если злоумышленник посетил (или обманул администратора при посещении): www.unsafe-site.com?action=delete-activity-log&log;_id=1;%20DROP%20TABLE%20wp_posts. Log_id содержит команду SQL, которая впоследствии вводится в $ sql и будет выполняться как:

DELETE from wp_wptuts_activity_log WHERE log_id=1; DROP TABLE wp_posts

Результат: вся таблица wp_posts удаляется. Я видел такой код на форумах, и в результате каждый, кто посещает их сайт, может обновлять или удалять любую таблицу в своей базе данных.

Если первые две ошибки были исправлены, то это затрудняет работу этого типа атаки, но не невозможно, и это не защитит от «атакующего», у которого есть разрешение на удаление журналов активности. Крайне важно защитить ваш сайт от SQL-инъекций. Это также невероятно просто: WordPress предоставляет метод подготовки. В этом конкретном примере:

function safe_delete_log( $log_id ){
    global $wpdb;
    $sql = $wpdb->prepare("DELETE from {$wpdb->wptuts_activity_log} WHERE log_id = %d", $log_id);
    $deleted = $wpdb->query( $sql )
}

Теперь команда SQL будет выполняться как

DELETE from wp_wptuts_activity_log WHERE log_id=1;

Санитарная обработка запросов к базе данных

Большинство сантиазаций могут выполняться исключительно с использованием глобального wpdb $ - в основном посредством метода его подготовки. Он также предоставляет методы для безопасного вставки и обновления данных в таблицах. Обычно они работают путем замены неизвестного ввода или сопоставления ввода с помощью заполнителя формата. Этот формат сообщает WordPress, какие данные он должен ожидать:

% s означает, что string% d обозначает целое число % f обозначает float

. Сначала мы рассмотрим три метода, которые не только дезинфицируют запросы, но и строят их для вас слишком.

Вставка данных

WordPress предоставляет метод $ wpdb-> insert (). Это оболочка для вставки данных в базу данных и обработки санитарии. Он принимает три параметра:

Table name - имя tableData - массив данных для вставки в качестве столбца -> value pairsFormats - массив форматов для соответствующего значения в массиве данных (например,% s,% d,% f)

Обратите внимание, что ключи данных должны быть столбцами: если есть ключ, который не соответствует столбцу, может возникнуть ошибка.

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

global $wpdb;
//
$user_id = 1;
$activity = 1;
$object_id = 1479;
$activity_date = date_i18n('Y-m-d H:i:s', false, true);
$inserted = $wpdb->insert(
     $wpdb->wptuts_activity_log,
     array(
        'user_id'=>$user_id,
        'activity'=>$activity,
        'object_id'=>$object_id,
        'activity_date'=> $activity_date,
      ),
     array (
        '%d',
        '%s',
        '%d',
        '%s',
     )
 );
 if( $inserted ){
    $insert_id = $wpdb->insert_id;
 }else{
    //Insert failed
 }

Обновление данных

Для обновления данных в базе данных у нас есть $ wpdb-> update (). Этот метод принимает пять аргументов:

Table name - имя tableData - массив данных для обновления в виде столбца -> value pairsWhere - массив данных для соответствия в виде столбца -> value pairsData Format - массив форматов для соответствующих «данных» 'valuesWhere Format - массив форматов для соответствующих' where 'values

Это обновляет любые строки, соответствующие массиву where со значениями из массива данных. Опять же, как и в $ wpdb-> insert (), ключи массива данных должны соответствовать столбцу. Он возвращает false при ошибке или число обновленных строк.

В следующем примере мы обновляем любые записи с идентификатором журнала «14» (который должен быть не более одной записи, так как это наш первичный ключ). Он обновляет идентификатор пользователя до 2 и операцию «редактируется».

global $wpdb;
$user_id=2;
$activity='edited';
$log_id = 14;
$updated = $wpdb->update(
     $wpdb->wptuts_activity_log,
     array(
        'user_id'=>$user_id,
        'activity'=>$activity,
     ),
     array('log_id'=>$log_id,),
     array( '%d', '%s'),
     array( '%d'),
 );
 if( $updated ){
    //Number of rows updated = $updated
 }

Удаление

Начиная с 3.4 WordPress также предоставил метод $ wpdb-> delete () для простого (и безопасного) удаления строк (строк). Этот метод принимает три параметра:

Table name - имя tableWhere - массив данных для соответствия в виде столбца -> value pairsFormats - массив форматов для соответствующего типа значений (например,% s,% d,% f)

Если вы хотите, чтобы ваш код был совместим с WordPress pre-3.4, вам нужно будет использовать метод $ wpdb-> prepare для дезинфекции соответствующего оператора SQL. Пример этого был приведен выше. Метод $ wpdb-> delete возвращает количество удаленных строк или false в противном случае, поэтому вы можете определить, было ли удаление успешным.

global $wpdb;
$deleted = $wpdb->delete(
     $wpdb->wptuts_activity_log,
     array('log_id'=>14,),
     array( '%d'),
 );
 if( $deleted ){
    //Number of rows deleted = $deleted
 }

esc_sql

В свете вышеприведенных методов и более общего метода $ wpdb-> prepare (), рассмотренного ниже, эта функция немного избыточна. Он предоставлен в качестве полезной оболочки для метода $ wpdb-> escape (), самого прославленного addlashes. Поскольку, как правило, более целесообразно и целесообразно использовать вышеуказанные три метода или $ wpdb-> prepare (), вы, вероятно, обнаружите, что вам редко нужно использовать esc_sql ().

В качестве простого примера:

$activity = 'commented';
$sql = "DELETE FROM {$wpdb->wptuts_activity_log} WHERE activity='".esc_sql($activity)."';";

Общие запросы

Для общих команд SQL, где (т. Е. Те, которые не вставляют, удаляют или обновляют строки), мы должны использовать метод $ wpdb-> prepare (). Он принимает переменное количество аргументов. Первый - это запрос SQL, который мы хотим выполнить со всеми «неизвестными» данными, замененными их соответствующим заполнителем формата. Эти значения передаются как дополнительные аргументы в том порядке, в котором они отображаются.

Например, вместо:

$sql = "SELECT* FROM {$wpdb->wptuts_activity_log} 
         WHERE user_id = $user_id
         AND object_id = $object_id
         AND activity = $activity
         ORDER BY activity_date $order";
$logs = $wpdb->get_results($sql);

у нас есть

$sql = $wpdb->prepare("SELECT* FROM {$wpdb->wptuts_activity_log} 
                WHERE user_id = %d
                AND object_id = %d
                AND activity = %s
                ORDER BY activity_date %s",
               $user_id,$object_id,$activity, $order  );
$logs = $wpdb->get_results($sql);

Метод подготовки делает две вещи.

    I применяет mysql_real_escape_string () (или addslashes ()) к вставленным значениям. В частности, это предотвратит выскальзывание значений, содержащих кавычки. nI применяет vsprintf () при добавлении значений в запрос, чтобы убедиться, что они отформатированы соответствующим образом (поэтому целые числа являются целыми числами, float - float и т. д.). Вот почему наш пример в самом начале статьи удалил все, кроме '1'.

Более сложные запросы

Вы должны найти, что $ wpdb-> prepare вместе с методами вставки, обновления и удаления вам действительно нужно. Иногда, хотя есть обстоятельства, когда требуется более «ручной» подход, иногда просто с точки зрения удобочитаемости. Например, предположим, что у нас есть неизвестный массив действий, для которых мы хотим все журналы. Мы * могли * динамически добавлять заполнители% s в SQL-запрос, но более прямой подход кажется более простым:

//An unknown array that should contain strings being queried for
$activities = array( ... );
//Sanitize the contents of the array
$activities = array_map('esc_sql',$activities);
$activities = array_map('sanitize_title_for_query',$activities);
//Create a string from the sanitised array forming the ier part of the IN( ... ) statement
$in_sql = "'". implode( "','", $activities ) . "'";
//Add this to the query
$sql = "SELECT* FROM $wpdb->wptuts_activity_log WHERE activity IN({$in_sql});"
//Perform the query
$logs = $wpdb->get_results($sql);

Идея заключается в применении esc_sql и sanitize_title_for_query к каждому элементу массива. Первая добавляет косые черты, чтобы избежать условий - аналогично тому, что делает $ wpdb-> prepare (). Второй просто применяет sanitize_title_with_dashes () - хотя поведение может быть полностью изменено с помощью фильтров. Фактический оператор SQL формируется путем взлома теперь дезинфицированного массива в строку, разделенную запятой, которая добавляется в часть IN (...) запроса.

Если ожидается, что массив будет содержать целые числа, тогда достаточно использовать intval () или absint () для дезинфекции каждого элемента в массиве.

«Белый список»

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

//An unknown array that should contain columns to be included in the query
$fields = array( ... );
//A whitelist of allowed fields
$allowed_fields = array( ... );   
//Convert fields to lowercase (as our column names are all lower case - see part 1)
$fields = array_map('strtolower',$fields);
//Sanitize by white listing
$fields = array_intersect($fields, $allowed_fields);
//Return only selected fields. Empty $fields is interpreted as all
if( empty($fields) ){
    $sql = "SELECT* FROM {$wpdb->wptuts_activity_log}";
}else{
    $sql = "SELECT ".implode(',',$fields)." FROM {$wpdb->wptuts_activity_log}";
}
//Perform the query  
$logs = $wpdb->get_results($sql);

Белый список также удобен при настройке части запроса ORDER BY (если это задано пользователем): данные могут быть заказаны только как DESC или ASC.

//Unknown user input (expected to be asc or desc)
$order = $_GET['order'];
//Allow input to be any, or mixed, case
$order = strtoupper($order);
//Sanitised order value
$order = ( 'ASC' == $order ? 'ASC' : 'DEC' );

LIKE-запросы

SQL-запросы LIKE поддерживают использование подстановочных знаков, таких как% (ноль или более символов) и _ (ровно один символ) при сопоставлении значений с запросом. Например, значение foobar будет соответствовать любому из запросов:

SELECT * FROM $wpdb->wptuts_activity_log WHERE activity LIKE 'foo%'
SELECT * FROM $wpdb->wptuts_activity_log WHERE activity LIKE '%bar'
SELECT * FROM $wpdb->wptuts_activity_log WHERE activity LIKE '%oba%'
SELECT * FROM $wpdb->wptuts_activity_log WHERE activity LIKE 'fo_bar%'

Однако эти специальные символы действительно могут присутствовать в поисковом термине - и поэтому, чтобы они не интерпретировались как подстановочные знаки - нам нужно избегать их. Для этого WordPress предоставляет функцию like_escape (). Обратите внимание, что это не предотвращает SQL-инъекцию, но только ускоряет символы% и _: вам все равно нужно использовать esc_sql () или $ wpdb-> prepare ().

//Collect term
$term = $_GET['activity'];
//Escape any wildcards
$term = like_escape($term);
$sql = $wpdb->prepare("SELECT* FROM $wpdb->wptuts_activity_log WHERE activity LIKE %s", '%'.$term.'%');
$logs = $wpdb->get_results($sql);

Функции обертки запроса

В приведенных примерах мы использовали два других метода $ wpdb:

$ wpdb-> query ($ sql) - он выполняет любой предоставленный ему запрос и возвращает количество затронутых строк. $ wpdb-> get_results ($ sql, $ ouput) - Выполняет выданный ему запрос и возвращает соответствующий набор результатов (то есть соответствующие строки). $ output задает формат возвращаемых результатов: nARRAY_A - числовой массив строк, где каждая строка является ассоциативным массивом, с ключом из столбца. ARRAY_N - числовой массив строк, где каждая строка представляет собой числовой массив. OBJECT - численный массив строки, где каждая строка является объектом строки. Default.OBJECT_K - ассоциативный массив строк (с ключом по значению первого столбца), где каждая строка является ассоциативным массивом.

Есть другие, о которых мы еще не упоминали:

$ wpdb-> get_row ($ sql, $ ouput, $ row) - Выполняет запрос и возвращает одну строку. $ row устанавливает, какая строка должна быть возвращена, по умолчанию это 0, первая соответствующая строка. $ output задает формат строки: nARRAY_A - строка представляет собой столбец => значение пара.ARRAY_N - строка представляет собой числовой массив значений. OBJECT - строка возвращается как объект. Default. $ wpdb-> get_col ($ sql, $ column) - Выполняет запрос и возвращает числовой массив значений из указанного столбца. $ column указывает, какой столбец следует возвращать как целое. По умолчанию это 0, первый столбец$ wpdb-> get_var ($ sql, $ column, $ row) - выполняет запрос и возвращает определенное значение. Столбец $ row и $ имеют значения, указанные выше, и укажите, какое значение нужно вернуть. Например, n1 $ activities_by_user_1 = $ wpdb-> get_var ("SELECT COUNT (*) FROM {$ wpdb-> wptuts_activity_log} WHERE user_id = 1");

Важно отметить, что эти методы - это просто обертки для выполнения SQL-запрос и форматирование результата. Они не дезинфицируют запрос - поэтому вы не должны использовать их в одиночку, когда запрос содержит некоторые «неизвестные» данные.


Резюме

В этом уроке мы довольно много рассмотрели, а санитария данных - важная тема для понимания. В следующей статье мы применим его к нашему подключаемому модулю. Мы рассмотрим создание набора функций-оболочек (подобно функциям типа wp_insert_post (), wp_delete_post () и т. Д.), Которые добавят слой абстракции между нашим подключаемым модулем и базой данных.