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.
