Безопасная разработка плагинов и тем


До сих пор вы уже познакомились с основами разработки плагинов и тем для WordPress. Сейчас самое время также рассмотреть вопросы безопасности при написании кода.

В этом уроке вы узнаете, как разрабатывать код с учетом требований безопасности.

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

Отказ от ответственности

Этот урок создан как введение в разработку плагинов и тем с учетом требований безопасности. Код, используемый в этом уроке, значительно упрощен. Не следует использовать какой-либо код из этого руководства в своих плагинах или темах.

Обязательно прочитайте полную документацию по безопасности в руководстве разработчика WordPress на странице https://developer.wordpress.org/apis/security/, чтобы убедиться, что вы соблюдаете правильные методы и процедуры.

Что значит разрабатывать с учетом требований безопасности?

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

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

При написании кода важно развивать мышление, ориентированное на безопасность, и думать о том, как ваш код может быть использован во вредоносных целях.

  • Не доверяйте никаким данным — будь то пользовательский ввод, данные стороннего API или даже данные в вашей базе данных. Всегда проверяйте, что они действительны и безопасны для использования.
  • В WordPress есть несколько API, которые могут помочь с распространенными задачами, такими как санитизация пользовательского ввода, проверка данных и экранирование вывода. Используйте эти API для проверки и санитизации данных вместо написания собственных функций.
  • Следите за распространенными уязвимостями и поддерживайте код в актуальном состоянии, чтобы предотвращать их появление.

На каком этапе разработки следует учитывать безопасность?

Безопасность следует учитывать на каждом этапе процесса разработки.

Как правило, наибольшее количество обнаруживаемых уязвимостей безопасности возникает на уровне PHP, который выполняется на сервере.

Поэтому этот урок будет посвящен наиболее распространенным мерам профилактики в вашем PHP-коде.

Санитизация входных данных

Одним из первых шагов при разработке должно быть обеспечение санитизации любых пользовательских данных. Это означает, что любые данные, передаваемые пользователем, например отправленные из формы или переданные в параметре URL, проверяются на безопасность использования.

В этом примере кода поля имени и адреса электронной почты отправляются из формы, которую создает плагин, а затем сохраняются в пользовательской таблице базы данных с именем form_submissions;

$name = $_POST['name'];
$email = $_POST['email'];
global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';

$sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
$result = $wpdb->query($sql);

Как видите, данные сохраняются непосредственно в базе данных без какой-либо санитизации.

Это означает, что если пользователь отправит имя John'; DROP TABLE form_submissions;--, будет выполнен SQL-запрос INSERT, за которым последует запрос DROP, и таблица будет удалена из базы данных!

В WordPress есть API санитизации, который можно использовать для санитизации входящих данных. Вы можете использовать функции sanitize_text_field и sanitize_email для санитизации полей имени и адреса электронной почты перед их использованием в запросе.

$name = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );

global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';

$sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
$result = $wpdb->query($sql);

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

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

Проверка данных (4:06)

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

Ненадежные данные могут поступать из разных источников: от пользователей, сторонних API и даже из вашей базы данных, особенно если кто-то другой изменил их. Даже администраторы сайта могут ошибиться и ввести некорректные или небезопасные данные, поэтому важно всегда проверять данные.

В этом примере функция удаления требует, чтобы числовой идентификатор был отправлен в callback-функцию admin-ajax:

add_action( 'wp_ajax_delete_form_submission', 'wp_learn_delete_form_submission' );
function wp_learn_delete_form_submission() {
    if ( ! isset( $_POST['id'] ) ) {
        wp_send_json_error( 'Invalid ID' );
    }
    $id = $_POST['id'];

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql    = "DELETE FROM $table_name WHERE id = $id";
    $result = $wpdb->get_results( $sql );

    return wp_send_json( array( 'result' => $result ) );
}

Здесь идентификатор напрямую используется в SQL-запросе без какой-либо проверки. Это снова означает, что если пользователь отправит идентификатор 1; DROP TABLE form_submissions;, после удаления будет выполнен тот же SQL-запрос DROP, а таблица будет удалена.

Чтобы предотвратить это, можно использовать функциональность приведения типов в PHP, чтобы гарантировать, что значение $id всегда является целым числом. Для этого перед именем переменной добавляется (int), например:

$id = (int) $_POST['id'];

Обратите внимание, что это сработает только в том случае, если первый символ строки, переданной через массив $_POST, может быть преобразован в целое число; в противном случае значение $id будет равно 0. В таком случае рекомендуется обновить код для обработки этой ситуации.

Код также следует ключевому принципу проверки данных: выполнять ее нужно как можно раньше.

if ($id === 0){
    // return early with an error
    return wp_send_json( array( 'result' => 'Invalid ID passed' ) );

}

Чтобы узнать больше о различных способах проверки данных, ознакомьтесь с разделом Проверка данных в документации разработчика WordPress.

Экранирование вывода

Еще один аспект безопасности — обеспечение безопасности любой информации, выводимой в браузер, включая текст, HTML- или JavaScript-код, а также данные из базы данных.

Даже если ваш код не отвечает за источник отображаемых данных, он отвечает за их безопасное отображение.

В этом примере код получает отправленные формы из базы данных, затем перебирает их и выводит данные в административном разделе панели управления WordPress:

    $submissions = wp_learn_get_form_submissions();
    ?>
    <div class="wrap" id="wp_learn_admin">
        <h1>Admin</h1>
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Email</th>
                </tr>
            </thead>
            <?php foreach ($submissions as $submission){ ?>
                <tr>
                    <td><?php echo $submission->name; ?></td>
                    <td><?php echo $submission->email; ?></td>
                    <td><a class="delete-submission" data-id="<?php echo $submission->id?>" style="cursor:pointer;">Delete</a></td>
                </tr>
            <?php } ?>
        </table>
    </div>
    <?php

Здесь есть три фрагмента данных, которые необходимо экранировать: поля $submission->name и $submission->email, а также $submission->id.

<td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Delete</a></td>

Для полей имени и адреса электронной почты можно использовать встроенную функцию экранирования WordPress esc_html(). Идентификатор можно экранировать, приведя его к целому числу, как это делается при проверке данных.

  <td><?php echo esc_html( $submission->name ); ?></td>
  <td><?php echo esc_html( $submission->email ); ?></td>
  <td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Delete</a></td>

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

Чтобы узнать больше о различных способах экранирования вывода, ознакомьтесь с разделом Экранирование данных в документации разработчика WordPress.

Предотвращение недействительных запросов

При каждом запросе важно проверять, что он действителен. Это означает проверку того, что запрос поступает из надежного источника.

Например, у вас может быть шорткод, отображающий форму, в которой пользователи могут отправить свои данные.

Функция отображения формы может выглядеть примерно так:

add_shortcode( 'wp_learn_form_shortcode', 'wp_learn_form_shortcode' );
function wp_learn_form_shortcode() {
    ob_start();
    ?>
    <form method="post">
        <input type="hidden" name="wp_learn_form" value="submit">
        <div>
            <label for="email">Name</label>
            <input type="text" id="name" name="name" placeholder="Name">
        </div>
        <div>
            <label for="email">Email address</label>
            <input type="text" id="email" name="email" placeholder="Email address">
        </div>
        <div>
            <input type="submit" id="submit" name="submit" value="Submit">
        </div>
    </form>
    <?php
    $form = ob_get_clean();
    return $form;
}
?>

Когда пользователь отправляет форму, данные передаются на сервер. Затем данные обрабатываются, санитизируются и сохраняются в базе данных.

add_action( 'wp', 'wp_learn_maybe_process_form' );
function wp_learn_maybe_process_form() {
    if (!isset($_POST['wp_learn_form'])){
        return;
    }

    $name = sanitize_text_field( $_POST['name'] );
    $email = sanitize_email( $_POST['email'] );

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
    $result = $wpdb->query($sql);
    if ( 0 < $result ) {
        wp_redirect( WPLEARN_SUCCESS_PAGE_SLUG );
        die();
    }

    wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
    die();
}

Поскольку форма может отображаться на любой странице, где используется шорткод, злоумышленник может попытаться отправить POST-запрос к форме — либо в поисках уязвимости в плагине, либо путем отправки множества запросов к форме.

Чтобы предотвратить это, можно проверить, что запрос поступает из надежного источника. Для этого можно реализовать так называемый nonce — число, используемое один раз.

Сначала в самой форме можно добавить поле nonce с помощью функции wp_nonce_field, передав этой функции действие nonce и имя nonce:

wp_nonce_field( 'wp_learn_form_nonce_action', 'wp_learn_form_nonce_field' );

При отображении формы во внешней части сайта в нее добавляется скрытое поле: имя nonce используется в качестве атрибутов id и name скрытого поля, а сгенерированный nonce — в качестве значения поля. Эти данные отправляются вместе с формой.

Затем в функции, обрабатывающей данные формы, можно проверить действительность nonce с помощью функции wp_verify_nonce, передав ей отправленное POST-полем значение nonce и действие nonce. Если результат проверки ложен, можно немедленно завершить выполнение, предотвратив дальнейшее выполнение кода.

    if ( ! wp_verify_nonce( $_POST['wp_learn_form_nonce_field'], 'wp_learn_form_nonce_action' ) {
        wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
        die();
    }

Всегда, когда ваш код выполняет веб-запрос — будь то перенаправление на новый URL, отправка данных POST в форму или выполнение AJAX-запроса, — необходимо проверять действительность запроса.

Чтобы узнать больше об использовании nonce в плагинах, ознакомьтесь с разделом Nonce в документации разработчика WordPress.

Предотвращение доступа неаутентифицированных пользователей

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

function wp_learn_delete_form_submission() {
    if ( ! isset( $_POST['id'] ) ) {
        wp_send_json_error( 'Invalid ID' );
    }
    $id = (int) $_POST['id'];

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql    = "DELETE FROM $table_name WHERE id = $id";
    $result = $wpdb->get_results( $sql );

    return wp_send_json( array( 'result' => $result ) );
}

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

В WordPress есть надежная система ролей и возможностей пользователей, которая позволяет использовать стандартные роли и возможности пользователей либо создавать собственные.

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

function wp_learn_delete_form_submission() {
    if ( ! current_user_can( 'manage_options' ) ) {
        return wp_send_json( array( 'result' => 'Authentication error' ) );
    }
    // rest of function code
}

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

Дополнительные материалы

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