Статьи

Возможности и одноразовые номера

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

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


Предположим, мы хотим, чтобы интерфейсная кнопка удаления быстро удаляла сообщения. Следующее создает ссылку везде, где мы используем wptuts_frontend_delete_link() внутри цикла.

01
02
03
04
05
06
07
08
09
10
function wptuts_frontend_delete_link() {
    $url = add_query_arg(
        array(
            ‘action’=>’wptuts_frontend_delete’,
            ‘post’=>get_the_ID();
        )
    );
 
    echo «<a href='{$url}’>Delete</a>»;
}

Затем обработать действие удаления:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
if ( isset($_REQUEST[‘action’]) && $_REQUEST[‘action’]==’wptuts_frontend_delete’ ) {
    add_action(‘init’,’wptuts_frontend_delete_post’);
}
 
function wptuts_frontend_delete_post() {
 
    // Get the ID of the post.
    $post_id = (isset($_REQUEST[‘post’]) ? (int) $_REQUEST[‘post’] : 0);
 
    // No post?
    if ( empty($post_id) )
        return;
 
    // Delete post
    wp_trash_post( $post_id );
 
    // Redirect to admin page
    $redirect = admin_url(‘edit.php’);
    wp_redirect( $redirect );
    exit;
}

Затем, когда пользователь щелкает ссылку «удалить», сообщение удаляется, и пользователь перенаправляется на экран администратора сообщения.

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


Когда пользователь регистрируется на вашем сайте WordPress, ему назначается роль: это может быть admin , editor или subscriber . Каждой роли назначены возможности, например, edit_posts , edit_others_posts , delete_posts или manage_options . Какой бы роли ни назначен пользователь, он наследует эти возможности: возможности назначаются ролям, а не пользователям .

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

Например, как правило, вы должны избегать :

1
2
3
if ( current_user_can(‘admin’) ) {
    // Do something that only users who can manage options should be able to do.
}

Вместо этого проверьте возможность (или возможности):

1
2
3
if ( current_user_can(‘manage_options’) ) {
    // Do something that only users who can manage options should be able to do.
}

Добавить и удалить возможности очень просто. WordPress предоставляет методы add_cap и remove_cap для объекта WP_Role . Например, чтобы добавить возможность ‘execute_xyz’ в роль редактора:

1
2
$editor = get_role(‘editor’);
$editor->add_cap(‘perform_xzy’);

Аналогично, чтобы удалить возможность:

1
2
$editor = get_role(‘editor’);
$editor->remove_cap(‘perform_xzy’);

Возможности роли хранятся в базе данных — поэтому вам нужно выполнить это только один раз (например, когда ваш плагин активирован или удален).

Если вы хотите, чтобы ваш плагин предоставлял настройки, позволяющие пользователям редактировать возможности других (возможности, которые связаны с функциональностью вашего плагина), то полезной функцией является использование get_editable_roles() который возвращает отфильтрованный массив ролей. Это не должно использоваться вместо проверки прав доступа, но позволяет пользователю вашего плагина ограничивать, какие роли могут редактироваться любой данной ролью. Например, редакторам может быть разрешено редактировать пользователей, но только авторов.

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

1
2
3
if ( current_user_can(‘edit_post’,61) ) {
    // Do something that only users who can edit post 61 should be able to do.
}

Возможность edit_post не назначена какой-либо роли (однако есть возможность примитива edit_posts ) — вместо этого WordPress проверяет, какие примитивные роли требуются этому пользователю, чтобы предоставить ему разрешение на редактирование этого сообщения. Например, если текущий пользователь является автором сообщения, ему требуется возможность edit_posts . Если это не так, им требуется возможность edit_others_posts . В обоих случаях, если публикация опубликована, им также потребуется возможность edit_published_posts . Таким образом, мета-возможности отображаются на одну или несколько примитивных возможностей.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
register_post_type(
    ‘event’,
    array(
        …
        ‘capabilities’=> array(
            // Meta capabilities
            ‘edit_post’=> ‘edit_event’,
            ‘read_post’=> ‘read_event’,
            ‘delete_post’=> ‘delete_event’,
 
            // Primitive capabilities
            ‘edit_posts’=> ‘edit_events’,
            ‘edit_others_posts’=> ‘edit_others_events’,
            ‘publish_posts’=> ‘edit_others_events’,
            ‘read_private_posts’=> ‘read_private_events’,
        ),
        …
    )
);

Затем, чтобы проверить, есть ли у текущего пользователя разрешение на редактирование сообщений:

1
2
3
if ( current_user_can(‘edit_events’) ) {
    // Do something that only users who can edit events should be able to do.
}

и проверить, может ли текущий пользователь редактировать определенное событие:

1
2
3
if ( current_user_can(‘edit_event’,$post_id) ) {
    // Do something that only users who can edit $post_id should be able to do.
}

Однако — edit_event (например, read_event и read_event ) является мета-возможностью, и поэтому нам нужно отобразить соответствующие примитивные возможности. Для этого мы используем фильтр map_meta_cap .

Логика объясняется в комментариях, но по существу мы сначала проверяем, что мета-возможности относятся к нашему типу поста события, и что переданный идентификатор поста относится к событию. Далее мы используем оператор switch для работы с каждой мета-возможностью и добавления ролей в массив $primitive_caps . Именно эти возможности понадобятся текущему пользователю, если ему будут предоставлены разрешения — и именно то, что они зависят от контекста.

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
add_filter(‘map_meta_cap’, ‘wptuts_event_meta_cap’,10,4);
 
function wptuts_event_meta_cap( $primitive_caps, $meta_cap, $user_id, $args ) {
    // If meta-capability is not event based do nothing.
    if ( !in_array($meta_cap,array(‘edit_event’,’delete_event’,’read_event’) ) ) {
        return $primitive_caps;
    }
 
    // Check post is of post type.
    $post = get_post( $args[0] );
    $post_type = get_post_type_object( $post->post_type );
    if ( ‘event’ != $post_type ) {
        return $primitive_caps;
    }
 
    $primitive_caps = array();
 
    switch( $meta_cap ):
        case ‘edit_event’:
            if ( $post->post_author == $user_id ) {
                // User is post author
                if ( ‘publish’ == $post->post_status ) {
                    // Event is published: require ‘edit_published_events’ capability
                    $primitive_caps[] = $post_type->cap->edit_published_posts;
 
                }
                elseif ( ‘trash’ == $post->post_status ) {
                    if (‘publish’ == get_post_meta($post->ID, ‘_wp_trash_meta_status’, true) ) {
                        // Event is a trashed published post require ‘edit_published_events’ capability
                        $primitive_caps[] = $post_type->cap->edit_published_posts;
                    }
 
                }
                else {
                    $primitive_caps[] = $post_type->cap->edit_posts;
                }
 
            }
            else {
                // The user is trying to edit a post belonging to someone else.
                $primitive_caps[] = $post_type->cap->edit_others_posts;
 
                // If the post is published or private, extra caps are required.
                if ( ‘publish’ == $post->post_status ) {
                    $primitive_caps[] = $post_type->cap->edit_published_posts;
 
                }
                elseif ( ‘private’ == $post->post_status ) {
                    $primitive_caps[] = $post_type->cap->edit_private_posts;
                }
            }
            break;
 
        case ‘read_event’:
            if ( ‘private’ != $post->post_status ) {
                // If the post is not private, just require read capability
                $primitive_caps[] = $post_type->cap->read;
 
            }
            elseif ( $post->post_author == $user_id ) {
                // Post is private, but current user is author
                $primitive_caps[] = $post_type->cap->read;
 
            }
            else {
                // Post is private, and current user is not the author
                $primitive_caps[] = $post_type->cap->read_private_post;
            }
            break;
 
        case ‘delete_event’:
            if ( $post->post_author == $user_id ) {
                // Current user is author, require delete_events capability
                $primitive_caps[] = $post_type->cap->delete_posts;
 
            }
            else {
                // Current user is no the author, require delete_others_events capability
                $primitive_caps[] = $post_type->cap->delete_others_posts;
            }
 
            // If post is published, require delete_published_posts capability too
            if ( ‘publish’ == $post->post_status ) {
                $primitive_caps[] = $post_type->cap->delete_published_posts;
            }
            break;
 
    endswitch;
 
    return $primitive_caps;
}

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

1
2
if ( ! current_user_can(‘delete_post’,$post_id) )
    return;

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

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

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

Поэтому, если кто-то отправит вам ссылку для удаления, она будет содержать свои одноразовые номера и поэтому не пройдет проверку одноразовых номеров. Обычно одноразовые номера используются только один раз, но реализация одноразовых номеров в WordPress немного отличается: фактически одноразовый номер меняется каждые 12 часов, и каждый одноразовый номер действителен в течение 24 часов. Вы можете изменить это с nonce_life фильтра nonce_life который фильтрует жизнь nonce в секундах (обычно 86400)

1
2
3
4
5
add_filter(‘nonce_life’, ‘wptuts_change_nonce_hourly’);
function wptuts_change_nonce_hourly( $nonce_life ) {
    // Change nonce life to 1 hour
    return 60*60;
}

(но 24 часа должны быть достаточно безопасными). Что еще более важно, одноразовые номера должны быть уникальными для самих инструкций и любых объектов, к которым они относятся (например, удаление сообщения и его идентификатора).

WordPress берет секретный ключ (вы можете найти его в вашем конфигурационном файле) и хеширует его вместе со следующими частями:

  • действие — это однозначно определяет действие. Это включает в себя действие и, если применимо, идентификатор объекта, к wptuts_frontend_delete_61 вы применяете действие: например, для удаления сообщения с идентификатором 61 мы можем установить одноразовое действие равным wptuts_frontend_delete_61
  • идентификатор пользователя — идентификатор, который идентифицирует идентификатор пользователя. Это делает одноразовый номер уникальным для каждого пользователя.
  • галочка — «галочка» отмечает прогресс во времени. Он увеличивается каждые 12 часов (или половину того, что такое nonce life). Это делает одноразовые номера каждые 12 часов.

Чтобы создать одноразовый номер, вы можете использовать wp_create_nonce($action) где $action объяснено выше. Затем WordPress добавляет тик и идентификатор пользователя и хэширует его с помощью секретного ключа.

Затем вы отправляете этот одноразовый номер вместе с действием и любыми другими данными, необходимыми для выполнения этого действия. Проверка одноразового номера очень проста.

1
2
// $nonce is the nonce value received with the action.
wp_verify_nonce($nonce, $action);

где $nonce — это полученное значение nonce, а $action — запрошенное действие, как указано выше. Затем WordPress генерирует одноразовый номер, используя $action и проверяет, соответствует ли оно указанной переменной $nonce . Если кто-то отправил вам ссылку, его одноразовый номер будет сгенерирован с его идентификатором, и поэтому он будет отличаться от вашего.

В качестве альтернативы, если одноразовый номер был опубликован или добавлен как переменная запроса, с именем $name :

1
check_admin_referer($action, $name);

Если одноразовый номер недействителен, он остановит дальнейшие действия и покажет « Вы уверены? сообщение.

WordPress позволяет легко использовать одноразовые номера: для форм вы можете использовать wp_nonce_field($action,$name) . Это создает скрытое поле с именем $name и одноразовым значением формы $action качестве его значения.

Для URL вы можете использовать wp_nonce_url($url,$action) . Это берет данный $url и возвращает его с добавленной переменной запроса _wpnonce , с сгенерированным одноразовым _wpnonce в качестве его значения.

В нашем примере:

01
02
03
04
05
06
07
08
09
10
11
12
function wptuts_frontend_delete_link() {
    $url = add_query_arg(
        array(
            ‘action’ => ‘wptuts_frontend_delete’,
            ‘post’ => get_the_ID();
        )
    );
 
    $nonce = ‘wptuts_frontend_delete_’ .
 
    echo «<a href='».wp_nonce_url($url,$nonce).»‘>Delete</a>»;
}

Который (для сообщения с идентификатором 61) генерирует одноразовый номер с действием wptuts_frontend_delete_61 . Затем, прямо над trash вызовом в wptuts_frontend_delete_post , мы можем проверить одноразовый номер:

1
check_admin_referer(‘wptuts_frontend_delete_’.$post_id, ‘_wpnonce’);

Использование одноразовых номеров в AJAX-запросах несколько сложнее. Одноразовые номера генерируются на стороне сервера, поэтому значение одноразового номера необходимо распечатать как переменную javascript для отправки вместе с запросом AJAX. Для этого вы можете использовать wp_localize_script . Предположим, вы зарегистрировали скрипт с именем wptuts_myjs который содержит запрос AJAX.

1
2
3
4
5
6
7
8
9
wp_enqueue_script(‘wptuts_myjs’);
wp_localize_script(
    ‘wptuts_myjs’,
    ‘wptuts_ajax’,
    array(
        ‘url’ => admin_url( ‘admin-ajax.php’ ), // URL to WordPress ajax handling page
        ‘nonce’ => wp_create_nonce(‘my_nonce_action’)
    )
);

Затем внутри нашего скрипта wptuts_myjs:

1
2
3
4
5
6
7
8
9
$.ajax({
    url: wptuts_ajax.url,
    dataType: ‘json’,
    data: {
        action: ‘my_ajax_action’,
        _ajax_nonce: wptuts_ajax.nonce,
    },
    …
});

Наконец, внутри вашего обратного вызова AJAX:

1
check_ajax_referer(‘my_nonce_action’);

Обычно достаточно одноразового номера в форме (или на запрос). Тем не менее, с WordPress контекст немного сложнее. Например, когда вы обновляете сообщение, WordPress будет выполнять проверки разрешений и одноразовых номеров — так что, вероятно, вам не нужно проверять одноразовый номер для вашей функции, подключенной к save_post который имеет дело с вашим пользовательским мета-блоком? Не так: save_post может быть запущен в других случаях, в разных контекстах или в событии вручную — фактически, когда wp_update_post вызывается wp_update_post . Чтобы быть уверенным, что данные, которые вы получаете, получены из вашего метабокса, вы должны использовать свой собственный одноразовый номер.

>

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

Поэтому при создании одноразового номера для вашего метабокса убедитесь, что вы дали ему уникальное имя:

1
2
3
4
5
6
7
8
function my_metabox_callback($post) {
    $name=’my_nonce_name’;
    $action=’my_action_xyz_’.$post->ID;
 
    wp_nonce_field($action,$name);
 
    // Your meta box…
}

Тогда для вашего мета-окна save_post :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
add_action(‘save_post’,’my_metabox_save_post’);
function my_metabox_save_post( $post_id ) {
    // Check its not an auto save
    if ( defined(‘DOING_AUTOSAVE’) && DOING_AUTOSAVE )
        return;
 
    // Check your data has been sent — this helps verify that we intend to process our metabox
    if ( !isset($_POST[‘my_nonce_name’]) )
        return;
 
    // Check permissions
    if ( !current_user_can(‘edit_post’, $post_id) )
        return;
 
    // Finally check the nonce
    check_admin_referer(‘my_action_xyz_’.$post_id, ‘my_nonce_name’);
 
    // Perform actions
}

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


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

Мы должны предположить, что злоумышленник может полностью проверить исходный код (будь то WordPress, тема или плагин) — и, если это так, простое скрытие ссылки ничего не делает: он может просто создать URL самостоятельно. Проверка возможностей предотвращает это, потому что даже с URL-адресами у них нет файлов cookie, которые дают им разрешение. Кроме того, одноразовые номера не позволяют им обманывать вас при посещении URL-адреса, требуя действительный одноразовый номер.

Итак, в качестве полностью эстетического упражнения мы включили проверку возможностей внутри функции wptuts_frontend_delete_link() :

01
02
03
04
05
06
07
08
09
10
11
12
function wptuts_frontend_delete_link() {
    if ( current_user_can(‘delete_post’, get_the_ID()) ) {
        $url = add_query_arg(
            array(
                ‘action’ => ‘wptuts_frontend_delete’,
                ‘post’ => get_the_ID();
            )
        );
 
        echo «<a href='{$url}’>Delete</a>»;
    }
}

Важно помнить, что возможности указывают на разрешение, а nonces проверяют намерение. И то, и другое необходимо для обеспечения безопасности вашего плагина, но одно не подразумевает другого. Иногда WordPress обрабатывает эти проверки для вас — например, при использовании настроек API . Однако при подключении к save_post необходимо выполнить эти проверки.

Удачного кодирования!