Аннотация. В Happy Addons for Elementor 3.50.0 (более 400 000 активных установок) есть уязвимость подделки серверных запросов (SSRF, CWE-918), доступная любому участнику (Contributor). Первопричина знакомая: значение, которое попадёт внутрь исходящего URL, очищается фильтром, оставляющим URL-метасимволы на месте, — выбранный очиститель не соответствует контексту, куда уходят данные. Разбираем дефектный код и корректное исправление как учебный пример безопасного программирования. По правилам координированного раскрытия шаги воспроизведения остаются у вендора; материал оборонительный и показывает, как распознавать, предотвращать и выявлять этот класс ошибок, а не как его использовать.
Ключевые слова: SSRF, CWE-918, очистка ввода, безопасность плагинов WordPress, Elementor, облачные метаданные, эшелонированная защита, минимальные привилегии.
1. Введение
Happy Addons for Elementor — набор виджетов для конструктора страниц Elementor, установленный более чем на 400 000 сайтах WordPress. При таких цифрах ставки меняются: один слабый строительный блок уезжает на каждый сайт, который добавит плагин, и одна ошибка наследуется в масштабе.
Подделка серверных запросов (SSRF, CWE-918) — один из самых разрушительных классов веб-уязвимостей, потому что превращает сервер приложения в прокси, говорящий изнутри границы доверия. Завладев этим прокси, злоумышленник дотягивается туда, куда интернет пускать не должны: до внутренних панелей администрирования, баз данных и, в первую очередь, до конечных точек облачных метаданных, раздающих краткоживущие учётные данные. Изучать этот случай стоит именно из-за его обыденности. Строка, очищающая ввод, делает ровно то, что заказал разработчик, — и при этом не делает ничего, чтобы остановить SSRF, потому что для этой задачи выбран не тот очиститель.
2. Затронутый компонент
| Плагин | Happy Addons for Elementor — slug happy-elementor-addons |
| Протестированная версия | 3.50.0 (актуальная на момент тестирования) |
| Активные установки | 400 000+ |
| Тип уязвимости | Подделка серверных запросов (SSRF) |
| CWE | CWE-918 Server-Side Request Forgery |
| Требуемая привилегия | Участник (Contributor; любой пользователь с правом edit_posts; получает nonce редактора Elementor) |
| CVSS 3.1 | ~6.5 (Средняя) AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L |
| Статус | Новая; подтверждена в коде. На момент публикации исправления нет — ведётся координированное раскрытие. |
3. Технический анализ
Аутентифицированное AJAX-действие ha_process_dynamic_select (wp_ajax_ha_process_dynamic_select) разрешает списки Mailchimp по переданному пользователем API-ключу. Оно защищено проверкой nonce (wp_verify_nonce('ha_editor_nonce')) и проверкой прав (current_user_can('edit_posts')). В ветке по умолчанию — не custom — ключ берётся из поля запроса global_api и проходит через sanitize_text_field():
// classes/select2-handler.php (process_mailchimp_list)
$choose_api = ... sanitize_text_field($_REQUEST['mailchimp_api_choose']) ...;
$global_api = ! empty($_REQUEST['global_api']) ? sanitize_text_field($_REQUEST['global_api']) : '';
$custom_api = ! empty($_REQUEST['mailchimp_api']) ? sanitize_key($_REQUEST['mailchimp_api']) : $global_api; // ветка "custom" использует sanitize_key (безопасно)
$current_api = $global_api;
if ($choose_api == 'custom') { $current_api = $custom_api; }
...
$options = Mailchimp_Api::get_mailchimp_lists($current_api);Всё решает то, какой очиститель отработает. sanitize_text_field() срезает теги и сворачивает пробелы, а URL-метасимволы вроде /, :, . и @ спокойно оставляет на месте. Для свободного текста это нормально. Но здесь значение — не свободный текст: оно уходит в построение исходящего URL. Правильный ответ уже показывает ветка custom: sanitize_key() пропускает только строчные буквенно-цифровые символы, дефисы и подчёркивания, и это закрыло бы дыру. Ветке global_api такой обработки не досталось.
Дальше по потоку get_mailchimp_lists() строит хост запроса прямо из сегмента «datacenter» ключа — всего, что идёт после первого -, — и отправляет серверный HTTP-запрос:
// widgets/mailchimp/mailchimp-api.php
$server = explode('-', self::$api_key);
if (!isset($server[1])) return 0;
$url = 'https://' . $server[1] . '.api.mailchimp.com/3.0/lists'; // $server[1] под влиянием злоумышленника
$response = wp_remote_post($url, ...);Хост собирается конкатенацией строк из ввода, на который влияет злоумышленник. Поскольку очиститель выше по потоку оставил URL-метасимволы нетронутыми, сегмент «datacenter» может нести символы, меняющие то, как итоговая строка разбирается как URL, — классическая предпосылка SSRF.
4. Как достигается уязвимость (концептуально)
API-ключ Mailchimp выглядит как <секрет>-<datacenter> (например, ...-us5), и плагин доверяет сегменту datacenter настолько, что вставляет его прямо в имя хоста. Код написан так, будто этот сегмент всегда остаётся коротким буквенно-цифровым кодом региона, а итоговый хост всегда оканчивается на .api.mailchimp.com.
Как только очиститель выше по потоку сохраняет URL-метасимволы, оба допущения рушатся. Пропустите в сегмент datacenter разделитель пути — и authority-часть URL (хост и необязательный порт, к которым клиент реально подключается) завершается раньше времени; добавляемый кодом суффикс .api.mailchimp.com больше не принадлежит хосту, а понижается в путь URL. Сервер подключается туда, куда указал автор ключа, а суффикс Mailchimp превращается в декоративный текст пути.
Чтобы добраться до обработчика, хватает уровня «Участник». Участник вправе открыть редактор Elementor на собственном черновике — именно там выдаётся nonce редактора, которого ждёт действие. Оба барьера — действительный nonce и право edit_posts — уже есть у обычного автора с низкими привилегиями. Ни администратора, ни взаимодействия с жертвой не нужно. Вдобавок часть полученного ответа возвращается разобранной как данные списка Mailchimp, поэтому фрагмент ответа может отразиться вызывающей стороне — и SSRF оказывается полуслепой, а не полностью слепой.
Мы намеренно не публикуем ни буквального значения ключа, ни тела запроса, ни готовой к копированию команды. Описанного выше механизма достаточно, чтобы понять проблему и защититься от неё, и недостаточно, чтобы превратить её в оружие.
5. Оценка воздействия
Участник способен заставить сервер слать запросы к внутренним или внешним хостам и портам по своему выбору. На практике это:
- Доступ к внутренним сервисам, отгороженным межсетевым экраном от публичного интернета, — интерфейсам администрирования, кэшам, базам данных.
- Запросы к конечным точкам облачных метаданных (link-local сервисы метаданных), способным раскрыть учётные данные и конфигурацию экземпляра.
- Картографирование и сканирование портов внутренней сети из доверенной точки.
- Частичное вытягивание ответа через путь разбора списков (полуслепой SSRF).
Вектор CVSS AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L (≈6.5, Средняя) вбирает сетевую доступность, низкую сложность атаки, низкое, но ненулевое требование к привилегиям у участника, отсутствие взаимодействия с пользователем и ограниченное, но реальное воздействие на конфиденциальность, целостность и доступность. Удерживает оценку на Средней, а не Высокой, одна лишь необходимость аутентификации — барьер тонкий: многие сайты принимают регистрации участников или работают в многоавторных конфигурациях, где такие учётные записи уже есть в количестве.
6. Устранение
Очищайте ключ так, чтобы URL-метасимволы не выживали, и проверяйте сегмент datacenter прежде, чем строить из него URL:
// Обрабатывайте ключ так же, как это уже делает ветка "custom":
$global_api = ! empty($_REQUEST['global_api']) ? sanitize_key($_REQUEST['global_api']) : '';
// И/или явно проверяйте сегмент datacenter перед построением URL:
$server = explode('-', $api_key);
if (!isset($server[1]) || !preg_match('/^[a-z]+[0-9]+$/', $server[1])) {
return 0; // отклонить любой datacenter, содержащий URL-метасимволы
}
$url = 'https://' . $server[1] . '.api.mailchimp.com/3.0/lists';Владельцам сайтов — до выхода исправленной версии:
- Обновитесь до исправленной версии, как только её выпустит вендор, и следите за журналом изменений плагина.
- Пересмотрите роли учётных записей. Уберите лишние учётные записи участников и авторов, отключите открытую регистрацию, если она не нужна, — уязвимость недостижима без аутентифицированной сессии с низкими привилегиями.
- Если функция динамического выбора Mailchimp у вас не используется, подумайте, нужен ли плагин на этом сайте вообще.
7. Обнаружение и усиление защиты для владельцев сайтов
- Фильтрация исходящего трафика (egress). Самая действенная мера против SSRF — ограничить исходящий трафик с веб-сервера. Запретите веб-уровню доступ к диапазонам RFC 1918,
localhostи особенно к link-local адресу метаданных; разрешите только те внешние хосты, которые сайту действительно нужны. - Усильте облачные метаданные. На облачных экземплярах требуйте усиленный/сессионный режим метаданных, чтобы одиночный неаутентифицированный GET не унёс учётные данные.
- Минимальные привилегии. Назначайте каждому пользователю минимально нужную роль и проверяйте, у кого есть право
edit_posts. - Мониторинг исходящих запросов. Следите по журналам сервера и egress-прокси за запросами от процесса WordPress к неожиданным хостам, внутренним диапазонам IP или конечной точке метаданных.
- Виртуальное исправление / WAF. Правило, отклоняющее запросы к уязвимому AJAX-действию с полем API-ключа, содержащим URL-метасимволы, выигрывает время как виртуальный патч. Это временная мера, а не замена исправления кода.
- Держите плагины обновлёнными и подпишитесь на ленты уязвимостей для используемых компонентов.
8. Более широкий урок
Повторяющаяся ошибка — относиться к очистке как к единому универсальному шагу, а не к контекстно-зависимому. sanitize_text_field() уместен для текста, который пойдёт на страницу; он неверен для значения, которое станет частью URL, пути файловой системы, команды оболочки или идентификатора SQL. Надёжная защита — валидация с учётом контекста вывода: решите, для чего предназначено значение, затем принудительно примените именно ту грамматику, которую допускает этот контекст, — здесь список разрешённого вида ^[a-z]+[0-9]+$ для кода datacenter.
Шире: как только ввод под влиянием пользователя конкатенируется в адрес назначения, к которому подключится сервер, считайте SSRF возможной — и держитесь этого, пока хост не проверен по списку разрешённого, а исходящий трафик не ограничен. Разбирать URL конкатенацией строк — хрупко. Строить его из проверенных компонентов, а затем разрешать и проверять итоговый хост перед подключением — вот что выдерживает нагрузку.
9. Ответственное раскрытие
Мы ведём этот случай в рамках координированного раскрытия. Вендор уведомлён конфиденциально и добросовестно; полные технические детали и воспроизведение переданы ему напрямую, чтобы он мог подтвердить и исправить дефект. По стандартной практике полный proof-of-concept остаётся вне этого публичного материала до выхода исправления или до истечения разумного срока раскрытия. После выпуска исправленной версии мы обновим статью.
Все описанные здесь тесты проводились Noroxi LLC в изолированной, привязанной к localhost лабораторной среде против общедоступного GPL-плагина. Никакие сторонние системы не затрагивались, а плагин впоследствии был удалён из лаборатории.
Ссылки
- MITRE CWE-918: Server-Side Request Forgery (SSRF) — https://cwe.mitre.org/data/definitions/918.html
- OWASP: Server-Side Request Forgery — https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
- OWASP SSRF Prevention Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
- Руководство разработчика WordPress: очистка/экранирование данных — https://developer.wordpress.org/apis/security/sanitizing/
- Справочник разработчика WordPress:
sanitize_key()— https://developer.wordpress.org/reference/functions/sanitize_key/ - Калькулятор CVSS v3.1 FIRST.org — https://www.first.org/cvss/calculator/3.1
Опубликовано в оборонительных и образовательных целях. Noroxi LLC проводит только авторизованные исследования безопасности; операционные детали эксплуатации удержаны до выхода исправления.
Эксплуатируется на этой неделе · по понедельникам
Новые записи KEV, свежие PoC, разбор WordPress и что сделать на неделе. Без аккаунта.