Аннотация. Большинство уязвимостей в плагинах WordPress — это не повреждение памяти, а обработчики, которые забыли спросить, вправе ли вызывающий сделать то, что он запросил. В Premium Addons for Elementor 4.11.110 таких два. Первый даёт участнику (Contributor) создавать опубликованные записи; второй даёт анонимному посетителю читать шаблоны библиотеки Elementor, которые платформа держит закрытыми. Ниже — код первопричины, концептуальный разбор того, почему достижима каждая проверка, и меры защиты. В рамках скоординированного раскрытия рабочий Proof of Concept в статью не входит, пока нет исправления.
Ключевые слова: нарушение контроля доступа, отсутствующая авторизация, CWE-862, проверка возможностей, nonce против возможности, Elementor, безопасность AJAX в WordPress.
1. Введение
Premium Addons for Elementor — распространённый набор виджетов для конструктора страниц Elementor, он работает более чем на 600 000 сайтах. Такой охват и делает его удачным примером. Ошибки здесь не экзотические: это самый частый класс уязвимостей плагинов WordPress — отсутствующая авторизация (CWE-862). Принцип, который они показывают, применим почти к любому плагину: nonce подтверждает намерение, а не право. Обработчик, который проверяет nonce и ни разу не вызывает current_user_can(), — ошибка частая и с высоким эффектом, и Premium Addons допускает её дважды.
Обе проблемы мы подтвердили в исходном коде плагина и воспроизвели в изолированной локальной (localhost) лаборатории на Elementor 4.3.3 и Premium Addons 4.11.110. Уровень честно средний: воздействие ограничено, но в обоих случаях это явное пересечение границы привилегий, которое стоит понимать и от которого стоит защищаться.
2. Затронутый компонент
| Плагин | Premium Addons for Elementor — слаг premium-addons-for-elementor |
| Тестируемая версия | 4.11.110 (текущая/последняя на момент тестирования) |
| Активные установки | 600 000+ |
| Статус | Свежая; подтверждена в коде и воспроизведена в локальной лаборатории |
A) handle_live_editor — отсутствующая авторизация → участник создаёт ОПУБЛИКОВАННЫЕ записи
| Тип | Отсутствующая авторизация / нарушение контроля доступа |
| CWE | CWE-862 Missing Authorization |
| Привилегия | Участник (любой пользователь с edit_posts; обычно не может публиковать) |
| CVSS 3.1 | ~5.4 (Средняя) AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L |
B) get_elementor_template_content — неаутентифицированное раскрытие шаблона Elementor
| Тип | Нарушение контроля доступа / раскрытие информации |
| CWE | CWE-862 / CWE-200 |
| Привилегия | Неаутентифицированный |
| CVSS 3.1 | ~5.3 (Средняя) AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N |
3. Технический анализ
3.1 Находка A — проверка nonce вместо проверки возможности
В includes/extras/live-editor.php (v4.11.110) два AJAX-действия регистрируются для каждого авторизованного пользователя:
add_action( 'wp_ajax_update_template_title', array( $this, 'update_template_title' ) ); // line 40
add_action( 'wp_ajax_handle_live_editor', array( $this, 'handle_live_editor' ) ); // line 41Соседний обработчик update_template_title() авторизацию делает верно — проверяет и nonce, и возможность:
public function update_template_title() {
check_ajax_referer( 'pa-live-editor', 'security' );
...
if ( ! current_user_can( 'manage_options' ) ) { // <-- capability check present
wp_send_json_error( 'Insufficient user permission' );
}
wp_update_post( ... );
}А handle_live_editor() проверку возможности убирает и оставляет только nonce:
public function handle_live_editor() {
check_ajax_referer( 'pa-live-editor', 'security' ); // nonce ONLY — no current_user_can()
if ( ! isset( $_POST['key'] ) ) { wp_send_json_error(); }
$key = sanitize_text_field( wp_unslash( $_POST['key'] ) );
$temp_type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : false;
wp_send_json_success( self::get_or_create_dynamic_template( $key, $temp_type ) ); // -> wp_insert_post(publish)
}В конце цепочки создаётся опубликованная запись:
$post_id = wp_insert_post( array(
'post_content' => '',
'post_type' => 'elementor_library',
'post_title' => $post_title, // 'PA Template | #' . substr(md5($key),0,4)
'post_name' => $post_name, // 'pa-dynamic-temp-' . $key (attacker-controlled slug)
'post_status' => 'publish', // <-- PUBLISHED
'meta_input' => self::get_dynamic_template_meta( $type ),
) );Nonce pa-live-editor — это не право, а всего лишь защита от CSRF (межсайтовой подделки запроса). Он подтверждает, что авторизованный пользователь отправил этот запрос сознательно, и только. Авторизацию WordPress держит в отдельном слое — возможностях (capabilities), — потому что «запрос подлинный» и «пользователю это разрешено» суть разные вопросы. Любой участник вправе открыть редактор Elementor на своём черновике, а именно там и выдаётся nonce pa-live-editor, так что пройти проверку nonce ему ничего не стоит. Дальше handle_live_editor() выполняет публикацию, которую участник не должен уметь выполнять. Выдаёт всё несогласованность: update_template_title() и check_temp_validity() проверяют manage_options, а handle_live_editor() и pa_get_editor_template() — нет.
3.2 Находка B — неаутентифицированный обработчик, защищённый только статусом записи
В includes/helpers/ajax-helper.php (v4.11.110) обработчик содержимого шаблона регистрируется ещё и для неаутентифицированных пользователей:
add_action( 'wp_ajax_get_elementor_template_content', array( $this, 'get_template_content' ) ); // line 37
add_action( 'wp_ajax_nopriv_get_elementor_template_content', array( $this, 'get_template_content' ) ); // line 38 (UNAUTH)Ни nonce, ни проверки возможности в обработчике нет. Единственный барьер — проверка статуса записи, и опубликованные записи проходят сквозь него:
public function get_template_content() {
$template = isset($_GET['templateID']) ? sanitize_text_field(wp_unslash($_GET['templateID'])) : '';
$is_ID = isset($_GET['is_id']) ? filter_var($_GET['is_id'], FILTER_VALIDATE_BOOLEAN) : false;
...
$post = get_post( $template );
if ( 'publish' !== $post->post_status ) { // only NON-published posts are gated
if ( ! current_user_can('manage_options') && get_current_user_id() !== (int) $post->post_author )
wp_send_json_error( 'Permission denied' );
}
$template_content = Helper_Functions::render_elementor_template( $template, $is_ID ); // renders & returns HTML
wp_send_json_success( array( 'template_content' => $template_content ) );
}Собственная модель Elementor считает элементы elementor_library непубличными: просмотр на фронтенде она ограничивает пользователями с edit_posts и делает это через block_template_frontend() на template_redirect. Этот AJAX-обработчик стоит рядом с той защитой и отрисовывает шаблон сам. Между тем для шаблонов библиотеки publish — это внутреннее состояние, а не заявление о том, что дизайн предназначен для публики. Здесь же этот статус оказывается единственным, что отделяет анонимного посетителя от отрисованного HTML любого шаблона библиотеки, в том числе тех, что никогда не размещались на публичной странице.
4. Как достигается дефект (концептуально)
Находка A. Достижимость держится на одном несоответствии. То, что проверяет обработчик, — действительный nonce pa-live-editor, — есть как раз у тех пользователей, которым до уязвимого участка доходить не следует. Nonce встраивается в редактор Elementor для каждого, кто способен его открыть, а открыть редактор на своём черновике позволяет лишь edit_posts — возможность участника. Барьер, на который полагается обработчик, тем самым преодолевается низкопривилегированным пользователем в рамках обычного, предусмотренного рабочего процесса. За барьером управление доходит до участка, который безусловно вставляет опубликованную запись. Недостающий шаг — тот, что делает update_template_title() и пропускает handle_live_editor(), — это вопрос «есть ли у пользователя manage_options?». Раз его никто не задаёт, участник выполняет публикацию, закреплённую ролевой моделью за редактором и администратором. Чтобы выйти на уязвимый путь, хватает обычного аутентифицированного доступа к редактору; детали на уровне запроса мы намеренно не приводим, пока идёт исправление.
Находка B. Тут нет и nonce, который нужно было бы добыть. Обработчик привязан к wp_ajax_nopriv_*, поэтому отвечает на анонимные запросы, а его единственная ветка, способная отказать, пропускается всякий раз, когда у целевой записи статус publish. Концептуальный разрыв в том, что «опубликованное» приравняли к «публичному». Для обычных записей это верно, но не для шаблонов библиотеки Elementor — класса объектов, который платформа сознательно держит за проверкой edit_posts. Поэтому неаутентифицированный актор выходит на путь «отрисовать и вернуть» для любого опубликованного шаблона библиотеки, просто сославшись на него. Неопубликованные шаблоны по-прежнему защищены, и это указывает прямо на причину — отсутствующую проверку возможности в ветке опубликованных записей. Здесь операционные детали запроса тоже ждут исправления.
5. Оценка воздействия
Находка A (CVSS ~5.4, Средняя). Учётная запись уровня участника создаёт произвольные опубликованные записи elementor_library со слагами по выбору злоумышленника и может повторять это в масштабе: несанкционированное создание контента, засорение базы данных и небольшой отказ в обслуживании за счёт массового создания. Потолок по-настоящему низкий. post_content пуст, заголовок следует фиксированному шаблону, meta_input — белый список, так что здесь нет ни хранимого XSS, ни удалённого выполнения кода. Честная сводка — нарушение контроля доступа среднего уровня: низкопривилегированный пользователь выполняет публикацию, зарезервированную за редактором и администратором. I:L/A:L отражает несанкционированное изменение целостности и неудобство массового создания, а PR:L — необходимость входа как участник.
Находка B (CVSS ~5.3, Средняя). Неаутентифицированное раскрытие содержимого шаблона библиотеки Elementor: дизайнов, встроенного текста, а также внутренних и подготавливаемых шаблонов, не связанных ни с одной публичной страницей, — всё это модель доступа Elementor ограничивает пользователями уровня участника и выше. Затрагивается только конфиденциальность (C:L, PR:N), без влияния на целостность или доступность, отсюда средне-низкий уровень.
6. Устранение
Находка A. Добавьте в handle_live_editor() проверку возможности, как у соседнего обработчика, и примените то же к pa_get_editor_template():
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Insufficient user permission' );
}Находка B. Перед отрисовкой добавьте проверку nonce и примените собственную политику просмотра Elementor, повторяя block_template_frontend():
check_ajax_referer( 'some-dedicated-nonce', 'security' );
if ( ! \Elementor\User::is_current_user_can_edit( $template ) ) { // i.e. edit_posts on the library item
wp_send_json_error( 'Permission denied' );
}Операторам сайтов стоит обновиться, как только производитель выпустит исправленную версию. До этого применяйте меры усиления из следующего раздела.
7. Обнаружение и усиление для владельцев сайтов
Полный рецепт эксплуатации не публикуется, поэтому признаки ниже выведены исключительно из механики:
- Проверяйте содержимое
elementor_library. Ищите опубликованные элементы библиотеки со слагами видаpa-dynamic-temp-*и заголовками по шаблонуPA Template | #XXXX, особенно от учётных записей уровня участника. Внезапный всплеск таких записей — самый явный признак эксплуатации Находки A. - Проверяйте, у кого есть уровень участника и выше. Находке A нужен аутентифицированный участник, поэтому действуйте по принципу наименьших привилегий: удаляйте неактивные учётные записи участников, требуйте надёжной аутентификации и не раздавайте
edit_postsшире необходимого. - Отслеживайте
admin-ajax.php. Повторяющиеся вызовы действийhandle_live_editorилиget_elementor_template_content, причём второе — от неаутентифицированных клиентов, стоит журналировать и ограничивать по частоте. - Считайте шаблоны библиотеки чувствительными. Пока Находка B не исправлена, не храните в шаблонах библиотеки Elementor секреты, учётные данные и неопубликованные тексты; опубликованные элементы библиотеки могут быть прочитаны без аутентификации.
- Виртуальное патчирование. Межсетевой экран веб-приложений или короткий must-use-плагин способны наложить проверку возможности на затронутые действия до выхода официального исправления — это концептуальный эквивалент недостающего
current_user_can()перед обработчиками. - Держите плагин обновлённым и подпишитесь на журнал изменений производителя, чтобы исправленная версия ставилась без задержки.
8. Более широкий урок
Каждая конечная точка, которая меняет состояние или возвращает данные, обязана ответить на три независимых вопроса, и верный ответ на один не закрывает остальные:
- Подлинный ли запрос? — nonce / CSRF-токен.
- Тот ли это пользователь, за кого себя выдаёт? — аутентификация.
- Разрешено ли пользователю делать именно это? — возможность (
current_user_can()).
В обеих находках ошибка одна: третий вопрос сводят к первому или пропускают совсем. Nonce легко достаётся любому, кто уже внутри обычного рабочего процесса продукта, и о привилегиях он не говорит ничего. А «опубликованное» — это статус, а не решение об авторизации; не позволяйте внутреннему состоянию объекта незаметно становиться его политикой доступа. Когда один обработчик защищён возможностью, а столь же мощный соседний — только nonce, эта несогласованность и есть уязвимость. Авторизуйте каждый уязвимый участок явно, прямо в нём.
9. Ответственное раскрытие
Эти находки обрабатываются в рамках скоординированного раскрытия. Производитель уведомлён в частном порядке и добросовестно. Полные технические детали Proof of Concept — точная последовательность запросов и значения полезной нагрузки — удерживаются из этой публичной статьи до появления исправления либо в течение 90 дней (смотря что наступит раньше), при согласовании сроков с производителем. Всё тестирование шло в изолированной, привязанной к localhost лаборатории против публичного GPL-плагина; к сторонним системам обращений не было, а тестовые плагины и пользователи были удалены после. Noroxi LLC проводит только авторизованные исследования безопасности и не помогает в эксплуатации действующих систем.
Источники
- CWE-862: Missing Authorization — https://cwe.mitre.org/data/definitions/862.html
- CWE-284: Improper Access Control — https://cwe.mitre.org/data/definitions/284.html
- CWE-200: Exposure of Sensitive Information to an Unauthorized Actor — https://cwe.mitre.org/data/definitions/200.html
- WordPress Developer Handbook — Checking User Capabilities — https://developer.wordpress.org/plugins/security/checking-user-capabilities/
- WordPress Developer Handbook — Nonces — https://developer.wordpress.org/plugins/security/nonces/
- OWASP — Broken Access Control — https://owasp.org/Top10/A01_2021-Broken_Access_Control/
- FIRST — калькулятор CVSS v3.1 — https://www.first.org/cvss/calculator/3.1
Опубликовано в защитных и образовательных целях. Noroxi LLC проводит только авторизованные исследования безопасности; операционные детали эксплуатации удерживаются до появления исправления.
Эксплуатируется на этой неделе · по понедельникам
Новые записи KEV, свежие PoC, разбор WordPress и что сделать на неделе. Без аккаунта.