Безопасность WordPress: уязвимости, атаки и защита

Безопасность WordPress: уязвимости, атаки и защита

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

По состоянию на 31 мая 2026 года актуальная ветка WordPress - 7.0, релиз вышел 20 мая 2026 года. Архив WordPress отдельно указывает, что безопасной и активно поддерживаемой считается самая свежая версия в ветке 7.0. Это хороший ориентир для администраторов, которые до сих пор держат старые установки ради совместимости. На практике я чаще вижу проблему не в ядре, а в плагинах. По данным Patchstack за 2025 год, 91% новых уязвимостей WordPress пришлись на плагины, 9% - на темы. Самые частые классы проблем: XSS, broken access control, CSRF, SQL injection, sensitive data exposure и arbitrary file upload.

В этой статье мы рассмотрим такие темы как устаревшие компоненты, XSS, SQL-инъекции, CSRF, ошибки контроля доступа, опасная загрузка файлов, XML-RPC, утечки конфигурации. Такой порядок нужен для понимания всей цепочки атаки. Хакер редко использует один прием. Обычно он сначала ищет версию и плагины, потом проверяет известные CVE, затем пробует слабые формы, REST API, загрузку файлов и доступ к служебным данным.

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

Рабочий набор инструментов для выявления уязвимостей Wordpress

Для WordPress удобно держать три группы инструментов. WP-CLI проверяет сайт изнутри. WPScan смотрит на сайт снаружи и использует базу уязвимостей WordPress. Nuclei и Nikto помогают найти типовые ошибки веб-сервера и известные шаблоны уязвимостей. WPScan официально описан как black-box scanner для проверки WordPress, плагинов, тем, доступных бэкапов, дампов базы и других признаков риска. Для получения данных об известных уязвимостях ему нужен API token.

# команда устанавливает системные пакеты для Ruby и Nikto
sudo apt update && sudo apt install -y ruby-full build-essential curl git nikto

# команда устанавливает WPScan
sudo gem install wpscan

# команда обновляет локальные метаданные WPScan
wpscan --update

# команда устанавливает WP-CLI
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar

# команда проверяет, запускается ли WP-CLI
php wp-cli.phar --info

# команда делает WP-CLI исполняемым файлом
chmod +x wp-cli.phar

# команда переносит WP-CLI в системный путь
sudo mv wp-cli.phar /usr/local/bin/wp

# команда устанавливает Nuclei через Go
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest

# команда обновляет шаблоны Nuclei
nuclei -update

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

Устаревшие ядро, плагины и темы

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

Проверка на наличие уявзимостей Wordpress начинается изнутри:

# команда показывает версию ядра WordPress
wp core version

# команда проверяет доступные обновления ядра
wp core check-update

# команда показывает плагины, для которых есть обновления
wp plugin list --update=available

# команда показывает темы, для которых есть обновления
wp theme list --update=available

# команда сверяет файлы ядра с контрольными суммами WordPress.org
wp core verify-checksums

# команда сверяет контрольные суммы плагинов из официального репозитория
wp plugin verify-checksums --all

wp core verify-checksums сравнивает установленные файлы ядра с контрольными суммами WordPress.org и делает это до загрузки WordPress. Это полезно после инцидента, когда есть риск, что злоумышленник изменил файлы ядра. Для плагинов есть отдельная команда wp plugin verify-checksums --all.

Снаружи сайт можно проверить c использованием инструментариев WPscan, Nuclei, Nikto:

# команда запускает поиск уязвимых плагинов и тем через WPScan
wpscan --url https://site.example -e vp,vt --plugins-detection mixed --api-token YOUR_TOKEN

# команда запускает Nuclei по WordPress и CVE шаблонам
nuclei -u https://site.example -tags wordpress,cves,misconfig -severity medium,high,critical -rate-limit 2

# команда запускает Nikto и сохраняет HTML-отчет
nikto -h https://site.example -output nikto.html -Format htm

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

Check Risk WAF здесь работает как защитный слой. Он не заменяет патч, но может фильтровать HTTP(S)-трафик, блокировать типовые атаки OWASP Top 10, применять поведенческий анализ.

XSS в формах, виджетах и админских страницах

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

Пример уязвимого кода:

<?php
// уязвимый код выводит параметр q без очистки и экранирования
echo '<div>Поиск: ' . $_GET['q'] . '</div>';

Защищенный вариант:

<?php
// код получает значение, снимает слеши WordPress и очищает текстовое поле
$q = sanitize_text_field( wp_unslash( $_GET['q'] ?? '' ) );

// код экранирует вывод в HTML контексте
echo '<div>Поиск: ' . esc_html( $q ) . '</div>';

sanitize_text_field() очищает строку от тегов, лишних пробелов и некоторых опасных последовательностей. Для вывода в HTML нужен esc_html(), для URL - esc_url(), для атрибутов - esc_attr(). Очищать данные на входе полезно, но окончательная защита от XSS строится на экранировании прямо перед выводом.

Быстрая проверка кода:

# команда ищет подозрительный прямой вывод GET и POST параметров
grep -R "echo .*_GET\|echo .*_POST\|print .*_GET\|print .*_POST" wp-content/themes wp-content/plugins -n

Для внешней проверки используйте Nuclei и WPScan, но не считайте сканер доказательством безопасности. XSS часто сидит в нестандартной бизнес-логике. Ее лучше ловить ревью кода и тестами.

На уровне WAF XSS обычно режется правилами, которые смотрят на опасные теги, обработчики событий, JavaScript-схемы и аномальные параметры. OWASP описывает WAF как фильтр HTTP-трафика, который применяет набор правил и обычно закрывает классы вроде XSS и SQL injection. Это не отменяет экранирование в коде, но снижает риск, пока разработчики чинят причину.

SQL injection в кастомных запросах

SQL injection появляется тогда, когда пользовательские данные попадают в SQL-строку как текст, а не как параметр. В WordPress ядро давно имеет безопасные функции, но кастомные плагины часто пишут запросы вручную через $wpdb.

Пример уязвимого кода:

<?php
global $wpdb;

// уязвимый код вставляет id прямо в SQL
$id = $_GET['id'];
$row = $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}orders WHERE id = $id" );

Защищенный вариант:

<?php
global $wpdb;

// код приводит id к целому числу
$id = absint( $_GET['id'] ?? 0 );

// код готовит SQL через параметр %d
$row = $wpdb->get_row(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}orders WHERE id = %d",
        $id
    )
);

$wpdb->prepare() готовит SQL-запрос к безопасному выполнению и поддерживает плейсхолдеры для строк, чисел и идентификаторов. В документации WordPress отдельно указано, что плейсхолдеры должны оставаться без кавычек, а аргумент должен передаваться для каждого плейсхолдера.

Поиск опасных мест:

# команда ищет ручные SQL-запросы в плагинах и темах
grep -R "\$wpdb->query\|\$wpdb->get_results\|\$wpdb->get_row\|\$wpdb->get_var" wp-content/plugins wp-content/themes -n

# команда ищет использование prepare
grep -R "\$wpdb->prepare" wp-content/plugins wp-content/themes -n

Закрытие строится на трех правилах. Числа приводим через absint() или явную валидацию. Строки передаем через %s. Для поиска LIKE используем $wpdb->esc_like(), а затем передаем результат в prepare(). Не нужно собирать куски SQL из $_GET и $_POST.

Check Risk WAF может отфильтровать SQLi-последовательности на входе, заметить аномальные параметры и отдать событие в журнал. Но WAF не знает всех допустимых переходов вашей бизнес-логики. Поэтому правильный фикс всегда остается в коде.

CSRF в действиях администратора

CSRF это уязвимость, которая заставляет авторизованного пользователя отправить запрос, который он не хотел выполнять. В WordPress это часто связано с admin_post, wp_ajax, страницами настроек и ссылками вида "удалить", "сохранить", "экспортировать". Если администратор открыт в панели и переходит по вредной ссылке, браузер сам приложит cookies.

Пример уязвимого кода:

<?php
add_action( 'admin_post_delete_order', 'delete_order' );

function delete_order() {
    $id = absint( $_GET['id'] ?? 0 );
    wp_delete_post( $id, true );
    wp_safe_redirect( admin_url( 'edit.php?post_type=shop_order' ) );
    exit;
}

Защищенный вариант:

<?php
// код создает ссылку с nonce
$url = wp_nonce_url(
    admin_url( 'admin-post.php?action=delete_order&id=' . $id ),
    'delete_order_' . $id
);

add_action( 'admin_post_delete_order', 'delete_order' );

function delete_order() {
    $id = absint( $_GET['id'] ?? 0 );

    // команда WordPress проверяет намерение через nonce
    check_admin_referer( 'delete_order_' . $id );

    // код проверяет право пользователя на удаление конкретной записи
    if ( ! current_user_can( 'delete_post', $id ) ) {
        wp_die( 'Forbidden', 403 );
    }

    wp_delete_post( $id, true );
    wp_safe_redirect( admin_url( 'edit.php?post_type=shop_order' ) );
    exit;
}

WordPress nonces нужны для защиты HTTP-запросов от CSRF. check_admin_referer() проверяет, что запрос пришел с корректным security nonce. Но nonce не заменяет проверку прав. Сначала проверяем намерение, потом полномочия.

Проверка кода:

# команда ищет обработчики, где часто забывают nonce и права
grep -R "admin_post_\|wp_ajax_\|register_rest_route" wp-content/plugins wp-content/themes -n

# команда ищет проверки nonce
grep -R "check_admin_referer\|check_ajax_referer\|wp_verify_nonce" wp-content/plugins wp-content/themes -n

WAF не всегда может надежно отличить легитимный CSRF от настоящего действия пользователя, потому что запрос формально выглядит валидным. Но Check Risk WAF может помочь на соседнем уровне: ограничить подозрительные источники, видеть всплески однотипных POST-запросов, включить мониторинг и быстро создать правило при атаке. В Check Risk WAF есть режим мониторинга и активной защиты, а также настройка уровней защиты.

Broken Access Control в REST API и AJAX

Broken Access Control - это ошибка, при которой пользователь делает то, что ему нельзя. В WordPress такое часто происходит в кастомных REST API маршрутах и AJAX-обработчиках. Разработчик проверяет, что пользователь залогинен, но не проверяет конкретное право. Или делает публичный endpoint для служебной операции.

Пример уязвимого кода:

<?php
add_action( 'rest_api_init', function () {
    register_rest_route( 'crm/v1', '/export', array(
        'methods'             => 'GET',
        'callback'            => 'crm_export_all',
        'permission_callback' => '__return_true',
    ) );
} );

Такой маршрут открыт всем. Если в нем возвращаются клиенты, заказы или email, это уже утечка.

Защищенный вариант:

<?php
add_action( 'rest_api_init', function () {
    register_rest_route( 'crm/v1', '/export', array(
        'methods'  => 'GET',
        'callback' => 'crm_export_all',

        // код разрешает доступ только пользователям с нужной capability
        'permission_callback' => function () {
            return current_user_can( 'manage_options' );
        },
    ) );
} );

register_rest_route() требует permission_callback, а с WordPress 5.5 отсутствие этого аргумента вызывает предупреждение. Для проверки прав используйте current_user_can(), лучше по capability и объекту, а не по названию роли. Документация WordPress прямо показывает проверки вроде current_user_can( 'edit_post', $post->ID ).

Проверка маршрутов:

# команда показывает публичный индекс REST API
curl -s https://site.example/wp-json/ | head

# команда ищет кастомные REST маршруты в коде
grep -R "register_rest_route" wp-content/plugins wp-content/themes -n

Для AJAX похожее правило. Каждый wp_ajax_* обработчик должен проверять nonce и capability. Если используется wp_ajax_nopriv_*, считайте его публичным endpoint и валидируйте все входные данные особенно жестко.

Check Risk WAF полезен как внешний контроль API. Он может ограничивать частоту запросов, фильтровать аномалии и давать видимость по endpoint-ам. Но права доступа должны жить в приложении. WAF не должен угадывать, какой пользователь может экспортировать базу клиентов.

Arbitrary File Upload и переход к RCE

Загрузка файлов опасна, когда сайт принимает файл, доверяет расширению и кладет его в директорию, где сервер может выполнить PHP. Частый сценарий: плагин загрузки резюме, форма обратной связи, импорт картинок или кастомный кабинет пользователя. Если туда попадает PHP-файл и веб-сервер его исполняет, уязвимость превращается в RCE.

Пример уязвимого кода

<?php
// уязвимый код доверяет имени файла и расширению
$target = WP_CONTENT_DIR . '/uploads/' . $_FILES['file']['name'];
move_uploaded_file( $_FILES['file']['tmp_name'], $target );

Защищенный вариант:

<?php
require_once ABSPATH . 'wp-admin/includes/file.php';

// код разрешает только конкретные MIME-типы
$allowed = array(
    'jpg|jpeg|jpe' => 'image/jpeg',
    'png'          => 'image/png',
    'pdf'          => 'application/pdf',
);

$file = wp_handle_upload(
    $_FILES['file'],
    array(
        'test_form' => false,
        'mimes'     => $allowed,
    )
);

if ( isset( $file['error'] ) ) {
    wp_die( esc_html( $file['error'] ) );
}

Проверка файловой системы:

# команда ищет PHP-файлы в uploads
find ./wp-content/uploads -type f -name "*.php" -print

# команда ищет подозрительные конструкции в wp-content
grep -R "eval(\|base64_decode\|gzinflate\|shell_exec\|passthru" wp-content -n

Запрет исполнения PHP в uploads для Nginx:

# правило запрещает выполнение PHP в каталоге загрузок
location ~* ^/wp-content/uploads/.*\.php$ {
    deny all;
}

Запрет исполнения PHP в uploads для Apache:

# правило запрещает PHP-файлы в uploads
<Directory "/var/www/site/wp-content/uploads">
    <FilesMatch "\.php$">
        Require all denied
    </FilesMatch>
</Directory>

После изменения конфигурации проверьте синтаксис и перезагрузите службу web сервера:

# команда проверяет конфигурацию Nginx
sudo nginx -t

# команда перезагружает Nginx после успешной проверки
sudo systemctl reload nginx

WAF помогает остановить часть вредных загрузок по MIME, расширению, сигнатурам веб-шеллов и аномальному телу запроса. Check Risk WAF в таком сценарии стоит включать перед формами загрузки и API импорта. Но файловая политика на сервере все равно обязательна. Сервер не должен исполнять то, что загрузил пользователь.

Авторизация, XML-RPC и перебор паролей

У WordPress есть очевидная точка атаки - wp-login.php. Есть и менее заметная - xmlrpc.php. XML-RPC нужен для удаленной работы старых клиентов и некоторых интеграций, но без явной необходимости он увеличивает поверхность атаки. Sucuri описывает риски XML-RPC как brute force через system.multicall и злоупотребление pingback для перегрузки целей.

Проверка веб приложения выглядет следующим образом:

# команда проверяет, доступен ли XML-RPC
curl -I https://site.example/xmlrpc.php

# команда показывает частые обращения к wp-login.php в access.log
grep "wp-login.php" /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -nr | head

# команда показывает частые обращения к xmlrpc.php
grep "xmlrpc.php" /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -nr | head

Если XML-RPC не нужен, закройте его на уровне веб-сервера.

Nginx:

# правило блокирует XML-RPC до загрузки WordPress
location = /xmlrpc.php {
    deny all;
    access_log off;
    log_not_found off;
}

Apache:

# правило блокирует XML-RPC через .htaccess или конфиг виртуального хоста
<Files "xmlrpc.php">
    Require all denied
</Files>

Для входа используйте MFA, сложные пароли, запрет общих учеток и минимум администраторов. Ограничение частоты запросов на /wp-login.php лучше ставить на уровне WAF или reverse proxy. Check Risk WAF здесь используется для защиты: антибот, rate limiting, поведенческий анализ и журналирование попыток входа помогают не отдавать все запросы PHP и базе данных.

Утечки конфигурации и слабые права файлов

wp-config.php содержит доступ к базе, соли, префикс таблиц и иногда секреты интеграций. Еще опаснее публичные архивы вроде backup.zip, дампы .sql, старые копии wp-config.php.bak и включенный debug на продакшене.

Проверка веб приложения на наличие опасных файлов:

# команда ищет потенциально опасные бэкапы и дампы
find . -type f \( -name "*.sql" -o -name "*.bak" -o -name "*.old" -o -name "*.zip" -o -name "*.tar.gz" \) -print

# команда проверяет права wp-config.php
ls -l wp-config.php

# команда ищет включенный debug
grep -n "WP_DEBUG\|WP_DEBUG_LOG\|WP_DEBUG_DISPLAY" wp-config.php

Базовое ужесточение прав:

# команда выставляет права 755 для директорий
find . -type d -exec chmod 755 {} \;

# команда выставляет права 644 для файлов
find . -type f -exec chmod 644 {} \;

# команда ужесточает доступ к wp-config.php
chmod 600 wp-config.php

WordPress в своей документации отдельно говорит, что wp-config.php должен быть защищен более строгими правами, чем обычные файлы. Там же указано, что права зависят от хостинга и пользователя веб-сервера, поэтому менять владельца нужно осознанно.

В wp-config.php для продакшена обычно нужны такие настройки:

<?php
// код запрещает редактирование файлов из админки
define( 'DISALLOW_FILE_EDIT', true );

// код отключает вывод debug ошибок пользователю
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_DISPLAY', false );

// код оставляет логирование только при отдельной необходимости
define( 'WP_DEBUG_LOG', false );

WAF не должен быть единственной защитой секретов. Но он может заблокировать запросы к типовым резервным файлам, архивам, дампам базы и служебным путям. Это полезно, когда файл случайно попал в web root и еще не удален.

Комплексные рекомендации

  • Обновляйте ядро, плагины и темы по регламенту. Старые компоненты дают атакующему готовую карту входа.
  • Удаляйте неиспользуемые плагины и темы. Отключить мало. Лишний код лучше убрать с диска.
  • Используйте MFA для администраторов. Пароль больше не должен быть единственным фактором входа.
  • Разделяйте роли. Редактору не нужен доступ администратора, а подрядчику не нужна постоянная учетная запись.
  • Делайте резервные копии и проверяйте восстановление. Бэкап без теста восстановления - это надежда, а не контроль.
  • Запрещайте исполнение PHP в uploads. Пользовательские файлы не должны становиться кодом.
  • Проверяйте кастомный код на sanitize, escape, prepare, nonce и current_user_can.
  • Логируйте события входа, изменения файлов, обновления и ошибки 4xx или 5xx. Без логов расследование превращается в угадывание.
  • Закрывайте XML-RPC, если он не нужен. Если нужен, ограничивайте доступ и следите за частотой запросов.
  • Используйте WAF как внешний защитный контур. Он фильтрует трафик до WordPress, помогает с ботами, сканерами, OWASP Top 10 и виртуальными патчами, но не заменяет исправление кода.

Заключение

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

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

wordpress
65
31.05.2026 15:00
ИП Елисеев Иван Владимирович
ИНН: 236401038208
Свидетельство о государственной регистрации программы для ЭВМ № 2026683774 от 04 августа 2026 г.
Продукт включен в реестр Российского ПО. Реестровая запись №32239 от 17.02.2026 г.
Регистрационный номер оператора, осуществляющего обработку персональных данных: 23-26-176186