Статьи

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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 );
}

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

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

1
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 и будет выполняться как:

1
DELETE from wp_wptuts_activity_log WHERE log_id=1;

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

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

1
2
3
4
5
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 теперь будет выполняться как

1
DELETE from wp_wptuts_activity_log WHERE log_id=1;

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

  • %s обозначает строку
  • %d обозначает целое число
  • %f обозначает плавание

Мы начнем с рассмотрения трех методов, которые не только очищают запросы, но и создают их для вас.

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

  • Имя таблицы — название таблицы
  • Данные — массив данных для вставки в виде столбца -> пары значений
  • Форматы — массив форматов для соответствующего значения в массиве данных (например, %s , %d , %f )

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
global $wpdb;
//
$user_id = 1;
$activity = 1;
$object_id = 1479;
$activity_date = date_i18n(‘Ymd 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() . Этот метод принимает пять аргументов:

  • Имя таблицы — название таблицы
  • Данные — массив данных для обновления в виде пар столбец-> значение
  • Где — массив данных для сопоставления в виде пар столбец-> значение
  • Формат данных — массив форматов для соответствующих значений данных
  • Где Формат — массив форматов для соответствующих значений «где»

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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() для простого (и безопасного) удаления строк. Этот метод принимает три параметра:

  • Имя таблицы — название таблицы
  • Где — массив данных для сопоставления в виде пар столбец-> значение
  • Форматы — массив форматов для соответствующего типа значения (например, %s , %d , %f )

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

1
2
3
4
5
6
7
8
9
global $wpdb;
$deleted = $wpdb->delete(
     $wpdb->wptuts_activity_log,
     array(‘log_id’=>14,),
     array( ‘%d’),
 );
 if( $deleted ){
    //Number of rows deleted = $deleted
 }

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

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

1
2
$activity = ‘commented’;
$sql = «DELETE FROM {$wpdb->wptuts_activity_log} WHERE activity='».esc_sql($activity).»‘;»;

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

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

1
2
3
4
5
6
$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);

у нас есть

1
2
3
4
5
6
7
$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);

Метод prepare делает две вещи.

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
//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 inner part of the IN( … ) statement
$in_sql = «‘».
 
//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() чтобы избежать терминов — аналогично тому, что делает $wpdb->prepare() . Второй просто применяет sanitize_title_with_dashes() — хотя поведение может быть полностью изменено с помощью фильтров. Фактический оператор SQL формируется путем внедрения очищенного массива в строку, разделенную запятыми, которая добавляется в часть запроса IN(...) .

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
//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.
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 .

1
2
3
4
5
6
7
8
//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’ );

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

1
2
3
4
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() .

1
2
3
4
5
6
7
8
9
//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 устанавливает формат возвращаемых результатов:
    • ARRAY_A — числовой массив строк, где каждая строка представляет собой ассоциативный массив, снабженный столбцами.
    • ARRAY_N — числовой массив строк, где каждая строка является числовым массивом.
    • OBJECT — числовой массив строк, где каждая строка является объектом строки. По умолчанию.
    • OBJECT_K — ассоциативный массив строк (по значению первого столбца), где каждая строка является ассоциативным массивом.

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

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

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


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