Аннотация. Версия 2.85 плагина WordPress 301 Redirects регистрирует AJAX-действие, которое проверяет nonce, но не проверяет права (capability). В итоге любой вошедший пользователь — хватит уровня Подписчика — может записать в глобальную опцию сайта (CWE-862). Мы сообщили об этом сопровождающему в рамках скоординированного раскрытия и придерживаем рабочий PoC до выхода исправления. Ниже — первопричина, стоящий за ней урок безопасного кода и шаги по обнаружению и усилению защиты, вытекающие прямо из механики.
Ключевые слова: безопасность WordPress, отсутствие авторизации, контроль доступа, CWE-862, AJAX nonce, проверка прав, граница привилегий.
1. Введение
301 Redirects (slug eps-301-redirects) управляет перенаправлениями URL из админки и работает более чем на 300 000 сайтов. Заметная их часть открывает публичную регистрацию, а значит выдаёт незнакомцам учётные записи уровня Подписчик. На любом таком сайте дефект, позволяющий Подписчику перешагнуть границу авторизации, заслуживает внимательного разбора — даже если прямого ущерба от него немного.
Дефект здесь — отсутствие авторизации, серверная половина нарушения контроля доступа. Код подтверждает, кто отправил запрос, через nonce, но ни разу не проверяет, вправе ли этот пользователь выполнять действие, через проверку прав. Это одна из самых частых ошибок в экосистеме плагинов WordPress, а данный случай — едва ли не самый наглядный учебный пример того, почему две проверки решают разные задачи.
2. Затронутый компонент
| Плагин | 301 Redirects — slug eps-301-redirects |
| Протестированная версия | 2.85 (актуальная на момент тестирования) |
| Активные установки | 300 000+ |
| Тип уязвимости | Отсутствие авторизации / Нарушение контроля доступа |
| CWE | CWE-862 Missing Authorization |
| Необходимые права | Любой аутентифицированный пользователь — Подписчик |
| CVSS 3.1 | ~4.3 (Низкая) AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N |
| Статус | Свежая; подтверждена на уровне кода и воспроизведена в лаборатории на актуальной версии |
3. Технический анализ
Плагин регистрирует AJAX-действие eps_dismiss_pointer, доступное аутентифицированным пользователям. Обработчик сначала проверяет nonce, затем пишет в глобальную опцию сайта eps_pointers. А вот вправе ли текущий пользователь менять эту опцию — не спрашивает нигде.
Код-первопричина — eps-301-redirects.php:
add_action('wp_ajax_eps_dismiss_pointer', array($this, 'dismiss_pointer_ajax')); // authenticated; any role
function dismiss_pointer_ajax() {
check_ajax_referer('eps_dismiss_pointer'); // nonce only — NO current_user_can()
if (!isset($_POST['pointer_name'])) { wp_send_json_error(); }
$pointers = get_option('eps_pointers');
$pointer = trim(sanitize_text_field(wp_unslash($_POST['pointer_name'])));
if (empty($pointers) || empty($pointers[$pointer])) { wp_send_json_error(); }
unset($pointers[$pointer]);
update_option('eps_pointers', $pointers); // SINK: site-global option write
wp_send_json_success();
}check_ajax_referer() проверяет nonce WordPress, а nonce — это анти-CSRF-токен. Он доказывает, что запрос пришёл со страницы, которую пользователь законно открыл, и что страница свежая. О привилегиях пользователя он не говорит ничего. За привилегии в WordPress отвечает current_user_can( $capability ), и этого вызова в обработчике нет вовсе. Поэтому перед глобальной update_option() остаётся единственный барьер: «вошёл ли вызывающий и есть ли у него действительный nonce». Подписчик проходит оба.
Регистрация действия только под wp_ajax_ (а не wp_ajax_nopriv_) правильно закрывает его от анонимных посетителей. Но «аутентифицирован» и «авторизован» — не одно и то же. Подписчик аутентифицирован, и в этом плагине большего, чтобы добраться до стока (sink), не требуется.
4. Как достигается уязвимость (концептуально)
Подписчик может запустить действие из-за второй, вышестоящей оплошности: nonce, который для этого нужен, раздаётся слишком широко.
Плагин ставит в очередь свои административные ресурсы — вместе с локализованным nonce — в методе plugin_resources(), подключённом к init и прикрытом одним лишь is_admin(). is_admin() сообщает только о том, нацелен ли запрос на админку. Это не проверка прав, и он возвращает true для любого вошедшего пользователя, открывшего страницу админ-области. Подписчикам открывать некоторые из таких страниц разрешено — скажем, собственный профиль, — и nonce лежит прямо в выводе подобных страниц.
На учебном уровне цепочка состоит из трёх звеньев:
- Обработчик, пишущий в глобальную опцию, прикрыт только nonce.
- Этот nonce попадает на страницы админки, доступные Подписчикам, потому что его выдача привязана к
is_admin(), а не к проверке прав. - В итоге у Подписчика оказывается действительный nonce для действия, задуманного лишь для администраторов, а обработчик при запуске привилегии больше не перепроверяет.
Итог — пересечение границы привилегий: пользователь, у которого есть только право read, меняет общесайтовую настройку, менять которую он не вправе. Последовательность запросов и полезные данные (payload) мы намеренно опускаем. Защитнику важен не готовый рецепт, а форма ошибки.
5. Оценка воздействия
На практике воздействие низкое. Через этот обработчик достижима единственная опция — eps_pointers, где хранятся подсказки интерфейса администратора при первичной настройке («pointers»). Подписчик может удалять оттуда записи и так прятать эти подсказки от администраторов. Ни раскрытия данных, ни потери доступности, ни возможности менять перенаправления или другие значимые для безопасности настройки по этому пути нет.
Вектор CVSS отражает это точь-в-точь: AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N (~4.3, Низкая) — доступно по сети, низкая сложность, низкие привилегии, без участия пользователя, область действия не меняется, воздействия на конфиденциальность и доступность нет, воздействие на целостность — лишь низкое. Публиковать находку стоит не ради радиуса поражения, а ради шаблона, который она вскрывает в плагине с 300 000 установок. Наведите тот же шаблон на более чувствительную опцию — и «низким» он уже не будет.
6. Устранение
Добавьте в обработчик проверку авторизации — рядом с той проверкой nonce, что уже есть. Плагин определяет фильтр прав, который использует в других местах — например, в add_widget(), — так что его повторное применение сохранит единообразие поведения:
function dismiss_pointer_ajax() {
check_ajax_referer('eps_dismiss_pointer');
if (!current_user_can(apply_filters('eps_301_redirects_capability','manage_options'))) {
wp_send_json_error(); // авторизация: только привилегированные пользователи
}
// ... существующая логика ...
}Владельцам сайтов стоит:
- Обновиться до исправленного выпуска, как только сопровождающий его выложит. На момент написания 2.85 остаётся актуальным выпуском, и исправления пока нет.
- А пока считать проблему низкорисковой, но настоящей, и применить усиление защиты ниже — в первую очередь на сайтах с открытой регистрацией.
7. Обнаружение и усиление защиты для владельцев сайтов
Всё, что ниже, вытекает прямо из разобранной механики.
- Сведите к минимуму низкопривилегированные учётные записи. Если публичная регистрация сайту не нужна, отключите её (Настройки → Общие → Членство). Уязвимости нужен аутентифицированный Подписчик, и чем меньше таких учёток, тем меньше поверхность.
- Применяйте принцип наименьших привилегий. Пересмотрите имеющиеся учётные записи и дайте каждому пользователю минимальную роль, которой хватает для работы. Заброшенные или неожиданные учётки Подписчиков — именно та поверхность атаки, что здесь важна.
- Следите за необычным вызовом
admin-ajax.php. Запросы с действием dismiss-pointer этого плагина от учёток, не являющихся администраторами, стоит отследить в журналах доступа. Шире: вызов аутентифицированными неадминистраторами ориентированных на администратора AJAX-действий — надёжная эвристика обнаружения. - Виртуальное исправление / WAF. Межсетевой экран веб-приложений или правило виртуального патча способны держать недостающую границу авторизации на периметре — например, ограничив это AJAX-действие привилегированными сессиями — пока плагин не обновлён. Это подход к смягчению, а не рекомендация какого-то одного продукта.
- Оставайтесь в курсе. Держите плагины обновлёнными и следите за примечаниями к выпускам сопровождающего, чтобы исправленная версия ставилась быстро.
8. Более широкий урок
Nonce — не средство авторизации. Это и есть главный вывод, и он верен далеко за пределами этого плагина:
check_ajax_referer()иwp_verify_nonce()отвечают на анти-CSRF-вопрос: пришёл ли запрос из законной, свежей загрузки страницы этим пользователем?current_user_can()отвечает на вопрос авторизации: разрешено ли этому пользователю это делать?
Каждому обработчику, меняющему состояние, нужны оба ответа. Рядом стоит второй принцип: не передавайте токены, открывающие доступ к возможностям, пользователям, у которых этих возможностей нет. Привязка выдачи nonce к is_admin(), то есть к проверке контекста, а не к проверке прав, расширяет круг тех, кто может действовать. Проектируя привилегированное действие, привязывайте и токен, и обработчик к одной проверке прав и запрещайте по умолчанию.
9. Ответственное раскрытие
Исследование велось в рамках скоординированного (ответственного) раскрытия. Сопровождающего 301 Redirects мы уведомили в частном порядке, а полный рабочий PoC остаётся за пределами этой публичной статьи до выхода исправления. Полные технические детали мы намерены опубликовать только после выхода исправленного выпуска или по истечении стандартного 90-дневного срока, согласовав сроки с сопровождающим, и запросим CVE.
Всё тестирование шло в изолированной, привязанной к localhost лаборатории против публичного GPL-плагина. Сторонних и рабочих систем мы не касались. Тестовый плагин и тестовые учётные записи затем удалили. Ничто в этой статье не предназначено для содействия атакам; она опубликована, чтобы разработчики и владельцы сайтов могли распознавать этот класс ошибок, предотвращать и обнаруживать его.
Источники
- CWE-862: Missing Authorization — https://cwe.mitre.org/data/definitions/862.html
- OWASP: Broken Access Control — https://owasp.org/Top10/A01_2021-Broken_Access_Control/
- WordPress Plugin Developer Handbook — Checking User Capabilities — https://developer.wordpress.org/plugins/security/checking-user-capabilities/
- WordPress Plugin Developer Handbook — Nonces — https://developer.wordpress.org/plugins/security/nonces/
- Справка по
current_user_can()— https://developer.wordpress.org/reference/functions/current_user_can/ - Калькулятор CVSS v3.1 на FIRST.org — https://www.first.org/cvss/calculator/3.1
Опубликовано в защитных и образовательных целях. Noroxi LLC проводит только авторизованные исследования безопасности; операционные детали эксплуатации не раскрываются до выхода исправления.
Эксплуатируется на этой неделе · по понедельникам
Новые записи KEV, свежие PoC, разбор WordPress и что сделать на неделе. Без аккаунта.