Это вторая часть серии о пользовательских таблицах базы данных в 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
}
|
esc_sql
В свете вышеприведенных методов и более общего $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 делает две вещи.
- Он применяет
mysql_real_escape_string()(илиaddslashes()) к вставляемым значениям. В частности, это предотвратит выпадение значений, содержащих кавычки, из запроса. - Он применяет
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() каждого элемента в массиве.
Whitelisting
В других случаях белый список может быть уместным. Например, неизвестный ввод может быть массивом столбцов, которые должны быть возвращены в запросе. Поскольку мы знаем, что такое столбцы базы данных, мы можем просто внести их в белый список — удалив любые поля, которые мы не распознаем. Однако, чтобы сделать наш код понятным для человека, мы должны учитывать регистр символов. Для этого мы преобразуем все, что мы получаем, в нижний регистр — поскольку в первой части мы специально использовали имена столбцов в нижнем регистре.
|
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);
|
Query Wrapper Функции
В примерах, которые мы рассмотрели, мы использовали два других метода $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() добавят слой абстракции между нашим плагином и базой данных.